12. Absence and failure
12.1 Optionals#
let found: User? = find(users, id)
let name = found.name if found != none else "nobody"
let user = find(users, id) else return
let count = counts[key] else 0T? holds a T or none. It is read with if let, while let, match, or else, whose fallback is a T, a return, or an error. There is no force unwrap and no T??. Comparison with none is allowed; nothing else reads through an optional.
12.2 Results#
func parse(text: str) -> Config!:
if text == "":
error(bad_input, "empty configuration")
return Config(...)
let config = parse(text)
let config2 = parse(text) catch failure:
recover default_config()
let config3 = parse(text) catch failure:
if failure.code == bad_input:
recover default_config()
error(failure.code, f"cannot load: {failure.message}")T! is a fallible function result, yielding a T or an owned Error. Inside an explicitly fallible function, operations propagate automatically: their success values compose normally, and the first failure leaves the expression. A nonfallible function must handle each fallible operation with catch. This is checked at compile time; a failure is never silently discarded. Ignoring a successful non-unit value still requires discard(...).
expression catch failure: protects its whole left expression, including nested arguments, receivers and conversions. The nearest handler runs first. Failures in its handler body go outward, to an enclosing handled operand or the declared fallible function. A handler must recover value, return, fail, trap, or leave an enclosing loop; a unit handler may fall through. error(code, message) raises an error to the same destination and cannot be caught by the handler that is currently executing it.
An optional try marker may cover a whole expression, with the same checked behavior. A leading marker includes binary operations, conditional branches and optional fallback, stopping before an attached catch. In an operator operand it has unary precedence; use parentheses to mark a larger operand. It must cover a fallible operation.
Arguments run once, left to right in source order, including named arguments. Short-circuit operators, optional fallback, conditional branches and match guards keep their usual laziness. Partial owned values are released before their handler starts. Propagation does not roll back mutations that already happened. A lambda or callback starts a fresh failure context: its declared or expected function type controls what its body may propagate, independently of the scope that created it. spawn evaluates arguments here; the worker's failure is observed by wait.
T! is not a storable type: locals, fields, parameters and container elements use Result[T] when they need to retain an outcome.
Result[T] is an ordinary owned enum, available in every module, with cases .success(value: T) and .failure(reason: Error). Result[T].capture(operation) invokes a func() -> T! once and returns its success or retained error; creating the callback does not execute it. Use a closure to supply arguments:
let pending = Result[Config].capture(() => parse(text))
match pending:
.success(config): use_config(config)
.failure(failure): print(failure.message)pending.get() returns T!, propagating the stored failure without consuming or changing pending. Copies and containers retain the active payload or error; dropping the last reference releases it. Error text survives the capture scope. Like any enum, a result can cross a worker boundary exactly when its payload can; the error and its text are copied to the worker. Result[unit] represents a stored operation without a value, and Result[T?] distinguishes absence from failure.
12.3 Errors#
Error has a code: ErrorCode and a message: str. Codes are declared as constants, let bad_input = ErrorCode.package(3), unique within a package by the package's name, so codes from two packages never collide. A Base package's errors cross unchanged (§16.3).
12.4 Traps#
A trap ends the program and cannot be caught: int overflow, division by zero, an index out of range, an int(x) that does not fit, an assert that fails, a mutation during iteration, a trap("message"), and out of memory. It writes trap: file:line:column: message to standard error and exits with status 1: the position is the statement the program was running, the innermost one inside a called function, and the interpreter and a built program name the same one. assert(condition) and assert(condition, "message") stay in every build and report assert failed, then the message when one is given.