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)?.nameOption-safe access ?. is different:
name := maybe_user?.nameThe 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
OptionnorResult; - when its unsuccessful case cannot fit the enclosing function;
- inside a
deferexpression.
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.