Skip to main content
Early returns are a best practice that promotes checking required conditions as early as possible so you get bad cases out of the way. To make this rule easy to follow, Forst has the ensure keyword. It allows you to declare what you expect to be true. If it is not, the function returns an error (or exits if you’re inside main). If the condition holds, Forst keeps that knowledge for the lines below, so you do not have to check again.

Stop unless a value is valid

This greet function must not run with an empty name.
If name is empty, greet returns EmptyName. return "hello, " + name runs only after the check passes. is Min(1) is the condition. else EmptyName{} is the error to return when it fails. See Return a named error for how to declare errors with fields.

Use the value after the check

After a successful check, Forst remembers that the condition held and narrows your variable. Later lines can use name without repeating the bound. Describe a shape lists built-in conditions such as LessThan, Min, Present, and Ok.

Handle multiple outcomes in the same function

Use if when both outcomes still belong in the function. Use ensure when the branch you’re on only makes sense if more checks pass. This handler ships a paid order. An unpaid order waits. A paid order must be unlocked and must have items:
If order.paid is false, fulfill returns "awaiting payment" and never runs those checks. If it is paid, a locked order returns OrderLocked and an empty cart returns EmptyCart. After both ensures pass, order.items[0] is safe. But facts derived from ensure statements don’t survive when you exit the branch. So after the if, Forst does not treat the order as unlocked or as having items. For reusable names for those rules, see Name a reusable rule.

Write a shorter check

Forst also offers shorthand forms to help with typical use cases. Take Go’s filepath.IsAbs which returns a bool. You can check that result with ensure and leave out is True():
If the path is relative, openFile returns an error and never opens the file. That keeps the open from depending on the process working directory. ensure filepath.IsAbs(path) is the same as ensure filepath.IsAbs(path) is True(). The check is the bool that filepath.IsAbs returned. Forst does not remember that path is absolute. Later lines still treat path as an ordinary String. If you want Forst to remember that path is absolute, put the Go call in a type guard on path. The same idea works for pointers, errors, and Result values. You write the value. Forst fills in the usual check.

Continue only when a pointer is empty

This code stores a value in slot only when nothing is there yet.
ensure !slot is the same as ensure slot is Nil(). If slot already holds a pointer, claim returns SlotAlreadyTaken and does not overwrite it. open waits until claim succeeds. ensure claim(7) is the same as ensure claim(7) is Ok(). If claim fails, open returns that same SlotAlreadyTaken error. claim has no success value. Checking the call is enough. ensure slot (no !) means the pointer is non-nil, the same as ensure slot is Present(). On a map or array, Present and Nil mean nilness, not emptiness. An empty map or [] still passes Present. Use is NotEmpty() or is Min(n) for length.

Use the value from a Result

Map lookups and fallible calls return Result. Check success before you use the value. After placeOrder returns a Result, this continues only on success:
ensure order is the same as ensure order is Ok(). On failure, the function that contains this ensure returns order’s error (except in main and Go tests). After the check, order is the successful value. The same pattern works when you need that value in the next check. Here in is the order request and catalog maps SKUs to quantities:
After ensure avail, avail is the quantity in the map. The next line can compare in.quantity with it. If the lookup failed, the handler returns the lookup error. If the quantity is too high, it returns InsufficientStock with the SKU, the request, and what was available. When a Go call returns err, write ensure !err. That is the same as ensure err is Nil(), and a failed check returns that error. See Functions that can fail.

Short forms

When the type is known, bare ensure and ensure ! expand to these checks. else stays optional in every case. Other values still need an explicit is condition, as name did in greet.

Accept several valid cases

Use or to join several acceptable checks on the same value. This status continues if it is either pending or processing:
If both checks fail, advance returns InvalidStatus. Write the value once. Follow it with is Pending(), then or Processing(). Chained constraints stay together before or. In ensure password is String.Min(3).Max(32) or UUIDV4(), the first option checks a length range. The second runs UUIDV4(). Inside a type guard, ensure cannot return a custom error with else. Use or so any of several conditions can satisfy the guard. Callers then attach the error at the call.

Check against a named type

Write the type name without parentheses after is to check membership in that type. This works for literal unions and for constrained aliases that use a dotted chain with no or alternatives.
A named constrained type lets a callee require the bound, and lets the caller prove it once. See Describe a shape for type HttpPort = Int.Min(1).Max(65535) and similar aliases. Omit parentheses for named types. Use parentheses for guards and assertion functions (Strong(), Pending()). For closed lists of allowed values, see List alternatives.

Exit from main

main starts the process. A failed ensure there ends the process with exit status 1. There is no caller to return an error to, so a named error after else is not allowed. Write ensure drive(80) when you only need that exit. Add else { ... } when you want to log or clean up first. The block runs on failure. The process still exits afterward. This example calls drive, then stop. The first check has no block. The second prints a line if stop fails.
If drive fails, the program writes to stderr and exits. stop never runs. If stop fails, the program prints failed to stop, then exits. Nothing after that ensure in main runs.

When a later write drops the fact

A successful ensure establishes a fact about a variable or field. Forst remembers that fact on later lines, so you do not re-validate before a call that requires it.
After ensure order is Shippable() else CannotShip{}, ship(order) runs without checking order.address.country again. Forst already knows order satisfied Shippable().

What drops the fact

A write to a field the check depends on drops the fact.
The write is legal. It drops Shippable because the country changed. Forst reports the problem at ship(order) with:
  • the dropped condition name (Shippable)
  • the line where it was established
  • the write that invalidated it
  • how to restore it (ensure order is Shippable())
Write ensure order is Shippable() again after changing dependent fields. A write to an unrelated field leaves the fact intact.
lastViewedAt is not part of Shippable, so ship(order) is still valid. Reading a field never drops a fact. Assigning nil to a pointer field (order.address = nil) drops presence facts on address and its child fields. Writing through an existing pointer (order.address.country = "US") changes the pointed-to value and does not clear presence on address.

Caveats

See the roadmap for work that is not finished yet. A fact survives an if / else split only when it holds on every path. Loops assume zero iterations may run, so facts from a loop body do not apply after the loop unless they are true even when the loop never runs. Calling a Forst function that mutates a field the check used drops that fact. Calls to Go packages, unsafe, or reflect drop facts on mutable arguments the callee can reach. The call itself remains legal. Writing through a pointer alias (alias := ℴ alias.address.country = nil) drops facts on the aliased value. Starting a goroutine (go worker(order)) or sending data that contains pointers through a channel clears facts on shared mutable storage. Forst map lookups return a Result. Use ensure avail before reading the value. Generated Go uses ordinary conditionals and error returns for ensure. A custom failure after else becomes a named Go error type. In main, a failed check exits the process instead of returning.

Examples

See the ensure and narrowing examples on GitHub: Continue with named errors or reusable conditions.

Return a named error

Give each expected failure a name and fields.

Name a reusable rule

User-defined predicates for domain rules.

Functions that can fail

Result returns and Ok / Err checks.

Describe a shape

Built-in constraints on primitive fields.