Errors and Optionals
Fallible Types
Failure is represented with fallible types, not exceptions.
func open(path: &str): File! {
if failed {
return error.new("std.io.not_found", "file not found")
}
return file
}
T! is a fallible type. It means the expression or function succeeds with T or fails with the built-in error payload.
T! = fallible T with built-in error
The failure type is not written at each call site. All fallible values use the same failure payload type, error. An error is an owned, move-only handle; it is not a pair of borrowed fields.
Rules:
erroris a named compiler built-in type declared by the selectedstd/errormodule withpub primitive type error, like the declarations ofstr,i32, andneverin their owning standard modules.erroris available in every type context without an import and cannot be shadowed or redeclared by ordinary source.- The spelling
errormay still be used as an ordinary value binding name. For example,catch errorbinds a local value namederror. T!always means successTor failureerror.void!is the sole fallible form whose success branch carries no payload. It represents normal completion or recoverable failure and remains an ordinary storable outcome value.Tmust not benever, including after alias expansion or generic substitution. Usevoid!when failure is recoverable but success carries no value.- The compiler-checked
std/errorcontract owns the exact construction and observation surface. Its behavior guide owns code policy, storage, context, and accessor behavior. The built-in type identity selects that validated surface; the compiler does not recognize a member spelling or rewrite an alias. erroris move-only and owns cleanup. It cannot be copied, and a returned view cannot outlive the handle.- Every fallible type
T!, includingi32!andvoid!, is move-only because its failure branch may own an error node. Mixed outcomes containing a fallible layer are move-only as well. - The ABI layout of
erroris specified in ABI and Layout. error!is not a valid function return type. In a fallible function,return error_valuemeans failure, soerrorcannot be used as the success type without ambiguity. This rule is checked after type aliases and through optional success layers such aserror?!.
Construction and observation are ordinary standard-library declarations. Their exact source and allocation policy remain outside the syntax and propagation semantics defined here.
Inside a function returning T!, a compatible function body result or return value returns the success value unless the value has type error. return error_value returns the failure value.
func write(file: &+File, text: &str): void! {
if failed {
return error.new("std.io.broken_pipe", "broken pipe")
}
return
}
Postfix ? unwraps fallible and optional values for propagation.
let file = File.open(path)?
For T!, expr? evaluates to the success value when expr succeeds. On failure, the current function, method, or closure returns the same error payload through its declared fallible layer.
For the corresponding behavior on T?, see Optional Propagation.
Example:
let file = File.open(path)?
This binds file to the successful File value. If File.open(path) fails, the current callable returns that error as if return error_value had been executed.
In a callable returning (U!)?, that explicit return meaning is presence containing inner failure. In a callable returning U?!, it is outer failure. The declared outcome order is never flattened or reordered by propagation.
Rules:
- Postfix
?is not an exception mechanism. - Applying
?to an existing move-only outcome place requiresmove place?. This canonical form moves the complete outcome first and then unwraps it. The source place is uninitialized on every continuation, including success, propagated failure, and propagated absence paths. - A newly produced outcome temporary needs no
move. Every stored fallible outcome requiresmove; optional outcomes remain structurally copyable when their payload is copyable. - Postfix
?does not perform stack unwinding. - Postfix
?onT!can be used only when the current function, method, or closure result type contains a fallible layer. - Postfix
?onT?can be used only when the current function, method, or closure result type contains an optional layer. - Propagation selects the matching declared outcome layer through recursive outcome injection. It does not require that layer to be outermost and does not change the order of composed layers.
- Postfix
?does not convertnoneintoerror. - Postfix
?does not converterrorintonone. - Scope-end cleanup and
dropbehavior still run as they would for an explicitreturn. - Error conversion is not needed for propagation because every fallible value fails with
error. throwis not part of the language.
An existing move-only outcome is invalidated before its tag is selected:
func require_name(): String? {
let maybe: String? = find_name()
let text = move maybe?
use(maybe) // error: use after move
return move text
}
Postfix ! forcefully unwraps fallible and optional values.
let file = File.open(path)!
let user = move maybe_user!
Rules:
- For
T!,expr!evaluates to the success value whenexprsucceeds. - For
T?,expr!evaluates to the present value whenexpris present. - Applying
!to an existing move-only outcome place requiresmove place!. The complete source outcome is moved before its tag is checked. A newly produced temporary does not requiremove; a copyable optional place may also be eliminated withoutmove. - If
expr!sees failure ornone, execution terminates immediately through the ordinary non-recoverable Nocter safety trap. - That path is exactly the ordinary Nocter safety trap used for bounds, arithmetic, and other
checked contract violations. Nocter does not print the
errorpayload, emit a fixed absence message, or translate the trap into entry-wrapper failure status1. - A forced-unwrap trap performs no stack unwinding or source-level cleanup. Live locals and statement temporaries are not dropped on that path.
- OS-provided signal names, process status, crash reports, and incidental output after a trap are outside the language contract.
expr!does not returnerrorornoneto the caller.expr!has result typeT.- One expression layer accepts only one postfix
?or!. To eliminate a second composed outcome layer, use an intermediate binding or explicit grouping such as(load()?)!. expr!is intended for tests, prototypes, and truly unrecoverable assumptions.- Normal code should prefer
?,catch, orotherwise. expr!is not stack unwinding.- Programs that need a stable message, exit code, or cleanup before termination must handle the
outcome with
catchorotherwiseand call an explicit process API after the required work.
Recoverable Failure and Non-Recoverable Termination
Fallible return, trap, and abort are distinct mechanisms.
return error_value = recoverable failure through T!
trap = non-recoverable program defect or violated runtime check
abort = immediate process termination
Rules:
- In a function returning
T!,return expris a failure return whenexprhas typeerror. - In a function returning
T!,return expris a success return whenexpris assignable toT. Tmust not beerror.- Fallible failure return follows normal
returncleanup for scopes it leaves. traphas typenever.trapis used for program defects, compiler-inserted safety checks, and impossible paths.- Out-of-bounds indexing, integer overflow in normal arithmetic, division by zero, invalid live
boolvalues, invalid enum tags, and explicit unreachable execution all trap. trapdoes not unwind the stack.aborthas typenever.abortterminates the process immediately and does not run Nocter cleanup.panicis not a language feature.- Nocter does not perform stack unwinding.
- Build modes must not disable these trap checks; see Safety Checks and Build Modes.
catch handles the failure side of a fallible expression.
let file = File.open(path) catch failure {
return failure.context("while opening the file")
}
expr catch error { ... } means:
- Evaluate
expr. - If
exprsucceeds, the wholecatchexpression evaluates to the success value. - If
exprfails, bind the failure value to the catch binding and execute thecatchblock. - If the block reaches its end, its result becomes the whole
catchexpression's value.
Local recovery can therefore compute a replacement and continue:
let port = configured_port() catch failure {
report(failure)
8080
}
Use _ when the failure payload is intentionally discarded:
operation() catch _ {
return fallback()
}
Rules:
catchapplies only to fallible values of typeT!.catchdoes not apply to optional valuesT?.- The catch binding has type
error. - The binding name after
catchis an ordinary local name.catch erroris conventional, butcatch erris also valid. catch _creates no binding._cannot be referenced, hovered, renamed, or used as a provenance origin.- Bare
catch { ... }is invalid; discarding the failure must be explicit. - The catch block is evaluated only on failure.
- Applying
catchto an existing move-only fallible place requiresmove place catch name { ... }. The complete fallible value is moved before selecting success or failure. A new temporary does not requiremove; there are no copyable fallible values. - A reachable catch block end must produce a value assignable to the fallible success type
T. - A catch block may instead leave the current control path with
return,break,continue, a call returningnever, or another terminating construct. - For
void!, an empty catch block recovers withvoid. - A trailing
T!is not flattened and a trailingerrordoes not implicitly fail again. Use?to propagate or an explicitreturn error_valueto replace the enclosing failure. catchis not exception handling.catchdoes not perform stack unwinding.- A recovering catch moves its block result into the surrounding destination, then drops the remaining catch-local values before continuing.
- A terminating catch runs the same scope-end cleanup that its explicit control flow would run.
- If a
catchblock terminates by calling aneverfunction, cleanup behavior is determined by thatneverfunction. The compiler does not add implicit unwinding. - The
catchclause belongs to the immediately preceding fallible expression. It is not a general handler after arbitrary expressions.
Postfix ? propagates the original failure.
catch is used for explicit local handling or error replacement.
func read_all(
path: &str,
): String! {
var file = File.open(path) catch _ {
return error.new("app.open_failed", "failed to open input")
}
let text = file.read_to_string() catch _ {
return error.new("app.read_failed", "failed to read UTF-8 input")
}
return move text
}
map_error is not a language operation. It may be provided as an ordinary standard-library API in the future, but the compiler does not special-case that name.
Fallible values are not pattern matched.
Rules:
matchdoes not apply toT!.if expr is Patterndoes not apply toT!.is ok(...)and failure patterns are not part of the language.okis not a reserved keyword.- Fallible values are handled with postfix
?andcatch.
Optional Types
Optional values use the type syntax T?.
T? = optional T
An optional value is either present with a T value or absent.
Inside a function returning T?, a compatible function body result or return value returns a present value and return none returns absence.
func lookup(name: &str): &str? {
if missing {
return none
}
return value
}
Rules:
T?is not spelled as a specialOption<T>type.noneis the optional absent literal.T?is copyable exactly whenTis copyable. The absent branch does not make an optional with a move-only present payload copyable.Tmust not bevoid, including after alias expansion or generic substitution. Optionalvoidhas no source value for its present branch; use an enum when that state distinction is required.Tmust not benever. Optional values contain data or absence; they do not capture a terminating control path as a payload.- A compatible function body result or
return valuein aT?function returns the present value. return nonein aT?function returns absence.- Postfix
?onT?propagatesnonethrough the current optional return layer. matchandif expr is Patterndo not apply toT?.some(value)is not language syntax.someis not a reserved keyword. It is contextual only at the start of a static opaque result type such assome Iterator { .Item = T }; in value position it remains an ordinary identifier.
Composing Optionals and Fallible Types
Optional and fallible type constructors may be composed explicitly.
Preferred source spelling:
T?! = fallible optional success
T?! means the computation can fail with error. If it succeeds, the success value is optional: present T or none.
Rules:
T!means a fallible success value.T?means an optional value.- Prefer
T?!in official style. expr?onT?!unwraps only the fallible layer and producesT?.expr catch error { ... }onT?!handles only failure and leaves the optional success layer.- A reachable catch fallback for
T?!therefore producesT?; aTresult constructs presence, whilenonepreserves absence. otherwiseapplied after thatcatchhandles only successful absence; it does not enter the catch block.- Applying
?again to thatT?propagatesnonethrough the current optional return layer. - The second application is written through an intermediate binding or grouping. Adjacent
expr??is invalid;(expr?)?exposes the two elimination boundaries explicitly. - In a function returning
T?!, a compatible function body result orreturn valuereturns success with a presentT. - In a function returning
T?!,return nonereturns success with absence. - In a function returning
T?!,return error_valuereturns failure witherror. Tmust not beerror. Use a wrapper type if anerrorpayload must be carried as successful optional data.- An optional layer must not have
voidas its eventual payload. Consequentlyvoid?!and(void!)?are invalid even thoughvoid!is valid. nevermust not be the eventual payload of any optional or fallible composition.- Other mixed forms must use parentheses.
(T!)?means an optional fallible value.- Every mixed outcome containing a fallible layer is move-only. Consequently
T?!and(T!)?are move-only regardless ofT; an outer optional does not make an owned failure copyable.
Recursive Outcome Injection
An expression at an authoritative contextual expected-type boundary is checked against the complete expected type by one outer-to-inner rule. This rule is the only implicit construction of optional and fallible layers; return checking is one use of the general rule.
Given an expression and an expected result type:
- 1. If the expression already has exactly the expected type, accept it unchanged. Do not add
another outcome layer. Exact
voidmeans normal completion rather than a transported value. - If the expected type is
U?,noneconstructs absence. Every other expression is recursively injected intoU, then wrapped as presence. - If the expected type is
U!, an expression of typeerrorconstructs failure. Every other expression is recursively injected intoU, then wrapped as success. - At expected
void, an expression of typevoidis evaluated as normal completion. At another non-outcome expected type, the expression must be assignable to that type under the ordinary contextual typing rules.
The exact-type check occurs before opening an outcome layer. Returning an existing complete outcome therefore preserves its tags rather than nesting or reinterpreting it. One optional layer and one fallible layer are the maximum supported depth, and error cannot be a success base type, so the injection path is unique.
The order of the expected type determines the meaning of contextual none and error:
| Expected type | Source expression | Constructed value |
|---|---|---|
T?! | value: T | success with present T |
T?! | none | success with absence |
T?! | failure: error | outer failure |
(T!)? | value: T | presence containing success T |
(T!)? | failure: error | presence containing inner failure |
(T!)? | none | outer absence |
void! | operation(): void | payloadless success after completion |
For example:
func cached_name(): (String!)? {
if cache_disabled {
return none
}
if load_failed {
return error.new("app.cache.load_failed", "failed to load cached name")
}
return String.copy("Nocter")
}
An expression whose type is already String! is injected only into the outer optional layer of (String!)?. An expression whose type is already (String!)? is accepted unchanged. The same recursive rule applies to initializers, assignments, arguments, aggregate payloads, fallbacks, and callable returns.
Outcome injection does not weaken ownership rules or manufacture a copy. An existing move-only binding still requires explicit move, whether it supplies the complete expected value or a payload that the injection wraps. A newly produced temporary is transferred into the constructed outcome normally. Injection is not a coercion, does not unwrap a source outcome, and does not participate in selecting an expected type.
For void!, no success payload is moved or copied. If the source void expression terminates with never, no success tag is constructed and control does not reach the destination.
Example:
func env(name: &str): &str?! {
if missing {
return none
}
if invalid_utf8 {
return error.new("std.process.invalid_encoding", "environment value is not UTF-8")
}
return value
}
Using a fallible optional:
let maybe_config = load_config()?
let config = move maybe_config?
use(config)
Handling failure and absence independently:
let home = env("HOME") catch error {
return report(error)
} otherwise {
"unknown"
}
Optional Propagation
Postfix ? propagates optional absence.
When expr has type T?, expr? unwraps the present T. If expr is none, the current function, method, or closure returns none through its optional return layer.
func require_home(): &str? {
let home = lookup("HOME")?
return home
}
Rules:
- Postfix
?onT?is valid when the current callable body's result type can carrynone, such asU?,(U?)!, or(U!)?. - In a function returning
(U?)!,noneis returned as successful absence, not as failure. - In a function returning
(U!)?,noneis returned as outer absence. - Postfix
?onT?is invalid in a function whose current return layer cannot carrynone. - Exact absence propagation uses
?. Absence defaulting and control flow other than returning the samenoneuseotherwise. otherwisedoes not propagate absence by itself; it selects a fallback block when the optional value isnone.
Optional Otherwise Expressions
Optional fallback uses otherwise.
let home = lookup("HOME") otherwise { "/tmp" }
let config = find_config(path) otherwise {
return error.new("app.config.missing", path)
}
load(config)
Rules:
expr otherwise { body }applies only whenexprhas typeT?.- Applying
otherwiseto an existing move-only optional place requiresmove place otherwise { body }. The complete optional value is moved before selecting presence or absence. A new temporary or copyable optional value does not requiremove. - If
expris present, the result is the containedT. - If
exprisnone, the fallback body is evaluated. - The fallback body must produce
T, or it may terminate the current control path withreturn, loop-localbreak/continue, ornever. - The fallback body follows the common body rule: statements first, then an optional result expression.
- The fallback body is evaluated only when needed.
otherwiseis an expression, not a declaration form.otherwisedoes not usesome/nonepatterns.- Evaluating
exprand the fallback body follows normal ownership rules. ??,let ... else, andvar ... elseare not Nocter syntax.
Chained fallback is written by nesting otherwise in the fallback body:
let port = env_int("PORT") otherwise {
config.default_port otherwise { 8080 }
}
Optional and Fallible Pattern Branching
is is reserved for enum variants only.
Rules:
if expr is Pattern { ... }applies only to enum values, and the pattern must be written asEnum.variant.T?values do not supportis none,is Type, oris Type(name).T!values do not supportis Error(name),is Type, oris Type(name).T?has noSome/Noneenum variants. The absence value is the keywordnone, usable in expressions such asreturn none.T!has no success/failure enum variants. Failure is the fallible return channel carrying anerrorvalue.
Optional Loops
Optional values are not automatically iterable. Collection iteration helpers may return T?, but there is no dedicated optional-loop syntax. Use otherwise { break } or otherwise { continue } inside an ordinary loop as specified by Control Flow.