Luce
Luce BaseSourceLuciaOS

12. Absence and failure

12.1 Optionals#

let found: User? = find(users, id)
let present = found != none
let user = find(users, id) else return
let count = counts[key] else 0

T? 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, and there is no marker to write: a call that can fail reads like any other. Ignoring a successful non-unit value is said with _ = value (§5.3).

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.

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, the condition as written, then the message when one is given: assert failed: limit > 5: the limit is too small.