Programming Language

Nocter

A self-contained systems language built around simplicity, encapsulation, and foolproof design.

/spec/language/errors-and-optionals.md

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:

  • error is a named compiler built-in type declared by the selected std/error module with pub primitive type error, like the declarations of str, i32, and never in their owning standard modules.
  • error is available in every type context without an import and cannot be shadowed or redeclared by ordinary source.
  • The spelling error may still be used as an ordinary value binding name. For example, catch error binds a local value named error.
  • T! always means success T or failure error.
  • 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.
  • T must not be never, including after alias expansion or generic substitution. Use void! when failure is recoverable but success carries no value.
  • The compiler-checked std/error contract 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.
  • error is move-only and owns cleanup. It cannot be copied, and a returned view cannot outlive the handle.
  • Every fallible type T!, including i32! and void!, 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 error is specified in ABI and Layout.
  • error! is not a valid function return type. In a fallible function, return error_value means failure, so error cannot be used as the success type without ambiguity. This rule is checked after type aliases and through optional success layers such as error?!.

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 requires move 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 requires move; optional outcomes remain structurally copyable when their payload is copyable.
  • Postfix ? does not perform stack unwinding.
  • Postfix ? on T! can be used only when the current function, method, or closure result type contains a fallible layer.
  • Postfix ? on T? 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 convert none into error.
  • Postfix ? does not convert error into none.
  • Scope-end cleanup and drop behavior still run as they would for an explicit return.
  • Error conversion is not needed for propagation because every fallible value fails with error.
  • throw is 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 when expr succeeds.
  • For T?, expr! evaluates to the present value when expr is present.
  • Applying ! to an existing move-only outcome place requires move place!. The complete source outcome is moved before its tag is checked. A newly produced temporary does not require move; a copyable optional place may also be eliminated without move.
  • If expr! sees failure or none, 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 error payload, emit a fixed absence message, or translate the trap into entry-wrapper failure status 1.
  • 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 return error or none to the caller.
  • expr! has result type T.
  • 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, or otherwise.
  • expr! is not stack unwinding.
  • Programs that need a stable message, exit code, or cleanup before termination must handle the outcome with catch or otherwise and 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 expr is a failure return when expr has type error.
  • In a function returning T!, return expr is a success return when expr is assignable to T.
  • T must not be error.
  • Fallible failure return follows normal return cleanup for scopes it leaves.
  • trap has type never.
  • trap is 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 bool values, invalid enum tags, and explicit unreachable execution all trap.
  • trap does not unwind the stack.
  • abort has type never.
  • abort terminates the process immediately and does not run Nocter cleanup.
  • panic is 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 expr succeeds, the whole catch expression evaluates to the success value.
  • If expr fails, bind the failure value to the catch binding and execute the catch block.
  • If the block reaches its end, its result becomes the whole catch expression'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:

  • catch applies only to fallible values of type T!.
  • catch does not apply to optional values T?.
  • The catch binding has type error.
  • The binding name after catch is an ordinary local name. catch error is conventional, but catch err is 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 catch to an existing move-only fallible place requires move place catch name { ... }. The complete fallible value is moved before selecting success or failure. A new temporary does not require move; 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 returning never, or another terminating construct.
  • For void!, an empty catch block recovers with void.
  • A trailing T! is not flattened and a trailing error does not implicitly fail again. Use ? to propagate or an explicit return error_value to replace the enclosing failure.
  • catch is not exception handling.
  • catch does 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 catch block terminates by calling a never function, cleanup behavior is determined by that never function. The compiler does not add implicit unwinding.
  • The catch clause 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:

  • match does not apply to T!.
  • if expr is Pattern does not apply to T!.
  • is ok(...) and failure patterns are not part of the language.
  • ok is not a reserved keyword.
  • Fallible values are handled with postfix ? and catch.

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 special Option<T> type.
  • none is the optional absent literal.
  • T? is copyable exactly when T is copyable. The absent branch does not make an optional with a move-only present payload copyable.
  • T must not be void, including after alias expansion or generic substitution. Optional void has no source value for its present branch; use an enum when that state distinction is required.
  • T must not be never. Optional values contain data or absence; they do not capture a terminating control path as a payload.
  • A compatible function body result or return value in a T? function returns the present value.
  • return none in a T? function returns absence.
  • Postfix ? on T? propagates none through the current optional return layer.
  • match and if expr is Pattern do not apply to T?.
  • some(value) is not language syntax.
  • some is not a reserved keyword. It is contextual only at the start of a static opaque result type such as some 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? on T?! unwraps only the fallible layer and produces T?.
  • expr catch error { ... } on T?! handles only failure and leaves the optional success layer.
  • A reachable catch fallback for T?! therefore produces T?; a T result constructs presence, while none preserves absence.
  • otherwise applied after that catch handles only successful absence; it does not enter the catch block.
  • Applying ? again to that T? propagates none through 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 or return value returns success with a present T.
  • In a function returning T?!, return none returns success with absence.
  • In a function returning T?!, return error_value returns failure with error.
  • T must not be error. Use a wrapper type if an error payload must be carried as successful optional data.
  • An optional layer must not have void as its eventual payload. Consequently void?! and (void!)? are invalid even though void! is valid.
  • never must 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 of T; 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. 1. If the expression already has exactly the expected type, accept it unchanged. Do not add another outcome layer. Exact void means normal completion rather than a transported value.
  2. If the expected type is U?, none constructs absence. Every other expression is recursively injected into U, then wrapped as presence.
  3. If the expected type is U!, an expression of type error constructs failure. Every other expression is recursively injected into U, then wrapped as success.
  4. At expected void, an expression of type void is 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 typeSource expressionConstructed value
T?!value: Tsuccess with present T
T?!nonesuccess with absence
T?!failure: errorouter failure
(T!)?value: Tpresence containing success T
(T!)?failure: errorpresence containing inner failure
(T!)?noneouter absence
void!operation(): voidpayloadless 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 ? on T? is valid when the current callable body's result type can carry none, such as U?, (U?)!, or (U!)?.
  • In a function returning (U?)!, none is returned as successful absence, not as failure.
  • In a function returning (U!)?, none is returned as outer absence.
  • Postfix ? on T? is invalid in a function whose current return layer cannot carry none.
  • Exact absence propagation uses ?. Absence defaulting and control flow other than returning the same none use otherwise.
  • otherwise does not propagate absence by itself; it selects a fallback block when the optional value is none.

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 when expr has type T?.
  • Applying otherwise to an existing move-only optional place requires move place otherwise { body }. The complete optional value is moved before selecting presence or absence. A new temporary or copyable optional value does not require move.
  • If expr is present, the result is the contained T.
  • If expr is none, the fallback body is evaluated.
  • The fallback body must produce T, or it may terminate the current control path with return, loop-local break / continue, or never.
  • The fallback body follows the common body rule: statements first, then an optional result expression.
  • The fallback body is evaluated only when needed.
  • otherwise is an expression, not a declaration form.
  • otherwise does not use some / none patterns.
  • Evaluating expr and the fallback body follows normal ownership rules.
  • ??, let ... else, and var ... else are 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 as Enum.variant.
  • T? values do not support is none, is Type, or is Type(name).
  • T! values do not support is Error(name), is Type, or is Type(name).
  • T? has no Some / None enum variants. The absence value is the keyword none, usable in expressions such as return none.
  • T! has no success/failure enum variants. Failure is the fallible return channel carrying an error value.

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.