Skip to content

Propagation

Use postfix question mark to carry None or Err through compatible function boundaries.

Updated View as Markdown

Postfix ? extracts a successful payload or returns the unsuccessful case from the enclosing function.

Result propagation

For Result[T, E], ? produces T from Ok(T) and returns Err(E).

fn load_name(id: int): string ! LoadError {
    user := load_user(id)?
    return user.name
}

Conceptually:

user := match load_user(id) {
    Ok(value) => value
    Err(error) => return Err(error)
}

The real lowering preserves type coercions, defers, transaction cleanup, and effect tracking around that control edge.

The target expression is evaluated once. Nothing after the ? in the current expression runs on the unsuccessful path.

Error compatibility

The propagated error must fit the enclosing function’s error type.

error AuthError { InvalidToken }
error DbError { ConnectionFailed }
error AppError = AuthError | DbError

fn authenticate(): User ! AuthError {
    // ...
}

fn handle(): User ! AppError {
    return authenticate()?
}

AuthError is a member of AppError, so propagation is valid.

When an internal function has no return annotation, several propagated error types can synthesize an error union. When a return annotation is present, its error side is the compatibility boundary and propagation does not widen it.

Unrelated errors require explicit conversion.

user := authenticate() catch {
    InvalidToken => RequestError.Unauthorized
}?

Option propagation

For Option[T], ? produces T from Some(T) and returns None.

fn first_name(users: []User): string? {
    user := users.get(0)?
    return user.name
}

The enclosing function must have an optional success result. An ordinary non-optional function cannot propagate None.

Mixed contracts

An optional propagation does not invent an error for a fallible function.

fn load(id: int): User ! LoadError {
    maybe := cache.get(id)
    // `maybe?` is invalid here unless the success type is also optional.
}

Convert absence explicitly.

user := cache.get(id).to_result(NotFound { id })?

A function can be both fallible and return an optional success.

fn find_remote(id: int): User? ! NetworkError {
    // Result[Option[User], NetworkError]
}

Inside it, option ? propagates None through the success side, while result ? propagates Err through the error side.

Chaining

Postfix ? binds tightly to its target.

name := load_user(id)?.name

Option-safe access ?. is different:

name := maybe_user?.name

The first can return early; the second produces another optional value and continues.

Use parentheses when applying ? to a long query or composite expression would otherwise make the target unclear.

Effects

Result propagation records the error effect and a call edge for diagnostics. Option propagation is ordinary absence control flow and does not by itself add the error effect.

Both can create early exits that run registered user defers and other compiler-managed cleanup.

Cleanup order is the same as for an explicit return: registered defers run in reverse registration order while control leaves their scopes. A defer that has not yet been reached is not registered and therefore does not run.

Restrictions

? is rejected:

  • on a value that is neither Option nor Result;
  • when its unsuccessful case cannot fit the enclosing function;
  • inside a defer expression.

Deferred cleanup must handle its own errors because propagation cannot restart an exit already in progress.

? also requires an enclosing function-like boundary. It cannot propagate out of a module initializer or turn an arbitrary block into a fallible boundary.

Readability

Use ? for a failure that the current function intentionally exposes without local action. Use match or catch when this layer should recover, retry, add context, translate the domain, or choose a fallback.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close