Documentation / Errors, options and assertions

Errors, options and assertions

Moth separates ordinary absence, recoverable failure and broken invariants. Many languages combine these cases through null values or exceptions, which hides part of a function's behaviour.

Use options for absence, Error! returns for expected failure and assert only when continuing would violate an invariant.

Choose the explanation level: Error values

Error values

An Error carries a message and an optional numeric code.

missing = Error("Missing value")
coded = Error("Missing value", 200)

The code defaults to 0.

Use the message to explain the failure. Use the code when a stable category is useful to callers or tools.

Error carries Moth's builtin structured error type.

Its public fields:

  • message String
  • code Int = 0
missing = Error("Missing value")
coded = Error(message = "Missing value", code = 200)

Contract

  • message carries the human-readable explanation.
  • code carries a stable integer code and defaults to 0.
  • Positional construction follows field order.
  • Named construction follows normal constructor argument rules.
  • Error is reserved by the language.
  • ErrorKind, ErrorLocation and StackFrame remain ordinary user-available names.

Error! in a function signature marks an error return channel. It does not create a panic path and it is not a separate ordinary value type.

Custom structs and other compatible types may also be used as explicit error slot types. The builtin Error type is the common language-owned surface and the only automatic target for checked numeric recovery.

Casts between String and Error are documented by Casts.

Choose the explanation level: Error returns

Error returns

A function that can fail adds one final return marked with !.

parse_number |text String| -> Int, Error!:
    if text is "":
        return! Error("Missing number")
    ;

    return 42
;

Normal return sends success values.

return! sends one error value.

The caller must handle that error by propagating it or catching it.

A fallible function marks one return slot with !.

parse_number |text String| -> Int, Error!:
    if text is "":
        return! Error("Missing number", 200)
    ;

    return 42
;

Signature contract

  • A function may declare at most one error-channel slot.
  • The error slot must be the final return slot.
  • Success slots appear before it.
  • A function may have success slots plus one error slot.
  • A function may also expose only an error slot.
  • Success return slots contain types only. The error slot may carry a channel.

The error slot type is explicit. Error! is common, but a compatible custom error type can be used when the function contract requires it.

Returning an error

return! expression leaves through the current function's error channel.

  • The current function must declare an error slot.
  • Exactly one error value is required.
  • The value must be compatible with the declared error type.
  • return! is terminal for the current path.
  • Normal success values use return.

Error-only functions

A function may declare only an error slot:

validate |ready Bool| -> Error!:
    if not ready:
        return! Error("not ready")
    ;
;
  • The arrow is required: -> Error!.
  • No success return value is required.
  • Normal fallthrough is valid when no error is returned.
  • A nested block may use return! too. The statement leaves through the enclosing error-only function's error channel and terminates that path.
Failure = |
    message String,
|

fail |message String| -> Failure!:
    return! Failure(message)
;

Checked numeric operations use automatic recovery only when the current error slot is the builtin Error! channel. Custom error slots do not receive an implicit conversion.

First-class public Result values and result pattern matching are outside the current language path. Fallible carriers are consumed by ! or catch at the call site.

Choose the explanation level: Error propagation

Error propagation

Add ! after a fallible call when the current function should pass the error to its caller.

wrapper |text String| -> Int, Error!:
    value = parse_number(text)!
    return value
;

On success, value receives the parsed number.

On failure, wrapper returns the error immediately.

The surrounding function needs a compatible error return slot.

Postfix ! propagates a fallible value's error through the enclosing function.

wrapper |text String| -> Int, Error!:
    value = parse_number(text)!
    return value
;

Contract

  • The handled expression must have a fallible error channel.
  • The enclosing function must declare an error slot.
  • The produced error type must be compatible with that slot.
  • On success, the expression yields its success values.
  • On failure, the current function returns the error immediately.
  • A no-success fallible operation may still use postfix ! as a statement.
  • Multi-success fallible calls may feed a matching multi-bind.
items ~{4 Int} = {}

~items.push(value)!

Postfix propagation is not panic syntax.

cast! uses the same outward error path for a fallible explicit cast, but its target and evidence rules remain owned by Casts.

Removed forms

Old inline fallback and bang-handler spellings are invalid.

-- INVALID: old inline fallback and bang-handler spellings
value = parse_number(text)!0
value = parse_number(text) err!

Use catch then ... or a block catch: handler for recovery.

Checked numeric recovery is compiler-owned control flow. Numeric operators do not become first-class source-visible Error! expressions.

Choose the explanation level: Catch and recovery

Catch and recovery

Use catch when the current code has a useful fallback.

count = parse_number(text) catch:
    then 0
;

Use catch |err|: when the message or code matters:

count = parse_number(text) catch |err|:
    io.line(err.message)
    then 0
;

A short fallback can stay on one line:

count = parse_number(text) catch then 0

Use then to provide the value expected by the surrounding assignment.

catch handles a fallible value locally.

Block forms

Use catch: when the error value is not needed.

value = parse_number(text) catch:
    then 0
;

Use catch |err|: when the handler needs the error.

value = parse_number(text) catch |err|:
    io.line(err.message)
    then 0
;

The error binding:

  • is immutable
  • is visible only inside the handler
  • must not shadow another visible name
  • has the fallible expression's declared error type

Inline forms

A simple recovery may stay on one logical line.

value = parse_number(text) catch then 0
other = parse_number(text) catch |err| then err.code

Inline catch:

  • uses then
  • stays on one logical line
  • may produce the receiver's required value list
  • binds the error only inside the fallback expression
  • does not contain a statement body
  • cannot combine with postfix !

Value requirements

When the handled expression has success values and the receiving site needs them, every reachable handler path must:

  • produce the required values with then
  • leave the enclosing function with return
  • leave through its error channel with return!

A handler that can fall through without producing or terminating is invalid.

value = parse_number(text) catch |err|:
    io.line(err.message)
    return 0
;

A multi-success recovery uses then with the matching arity:

name, score = load_user(id) catch |err|:
    io.line(err.message)
    then "guest", 0.0
;

A fallible operation with no success values does not need then.

items ~{4 Int} = {}

~items.push(value) catch:
    io.line("push failed")
;

Context and conflicts

  • catch and postfix ! are alternative handling paths.
  • One expression cannot use both.
  • catch handles only the selected fallible expression.
  • Catch bodies are statement-like and are rejected inside conditions, templates and constants.
  • First-class fallible carriers cannot be stored without handling.

then and closed receiving sites are specified by Value-producing if.

Choose the explanation level: Options

Options

Use T? when a value may be missing and that absence is normal.

nickname String? = none

A normal String can be used where String? is expected.

Inspect a present value with is |name|:

display_name = if nickname is |name| then name else "guest"

Use postfix ? inside a function that also returns an optional value:

get_display_name |id String| -> String?:
    user = find_user(id)?
    return user.name
;

If find_user returns none, the current function returns none too.

T? is an optional value that may contain T or may be absent.

none is the only special absent value.

name String? = none
fallback String? = "guest"

Contextual wrapping

A value of T can be used where T? is expected.

none requires an immediate optional receiving context. It is not a standalone inferred type.

Optional values are not automatically unwrapped.

Present-value inspection

Inline inspection:

display_name = if maybe_name is |name| then name else "guest"

Block inspection:

display_name = if maybe_name is |name|:
    then name
else
    then "guest"
;

Statement inspection:

if maybe_name is none:
    io.line("missing")
;

Full option match:

label = if maybe_name is:
    "Priya" => then "Hi Priya"
    |name| => then name
    none => then "guest"
;

Option patterns support:

  • none
  • compatible literal present-value patterns
  • compatible relational present-value patterns
  • |name| to capture any present value
  • guards
  • else =>

An option match can be exhaustive without else => when it has both an unguarded none arm and an unguarded present-value capture.

Direct fallback syntax such as maybe_name else then "guest" is invalid.

Absence inspection

Statement if can test for absence:

if maybe_name is none:
    io.line("missing")
;

Equality and ordering

Options support equality comparisons:

present = maybe_name is "Priya"
absent = maybe_name is none
same = a is b

Two options can be compared only when they have the same option type and their inner type supports equality. Two absent values compare equal. Present values compare through the inner type's equality rule, and a present value never equals none.

Ordering operators such as < on optional values are not supported.

Postfix propagation

Postfix ? unwraps a present value or returns none from the enclosing function.

get_display_name |id String| -> String?:
    user = find_user(id)?
    return user.name
;

The exact contract is:

  • the operand must be optional
  • the enclosing function must have exactly one success return slot
  • that success slot must be a compatible optional type
  • the present payload becomes the expression result
  • absence returns none immediately
  • ? cannot be followed by catch

Options are compiler-owned value shapes. Source code does not construct public Option variants.

Choose the explanation level: Assertions

Assertions

Use assert for a condition that must already be true.

assert(index < items.length())
assert(index < items.length(), "index must be in bounds")

The message is optional. When you add one it is an ordinary String? value, including a local string, runtime template snapshot or infallible string-producing call. It is still fully checked and normalized when the condition is the literal true. The message is normalized through the ordinary AST template path before it is discarded after final AST type/TIR validation, so its temporary AST-local TIR is removed with the message and no TIR reference, runtime handoff, reactive metadata, generated function or other runtime fact survives. JavaScript and HTML evaluate active messages only on the failure edge. HTML-Wasm accepts an unreachable literal-true runtime message but currently rejects reachable runtime message construction.

Message construction must not escape its evaluation through !, ?, return, break or continue.

A failed assertion stops the current program path.

It is not a recoverable error and cannot be caught.

Use Error! for failures callers are expected to handle. Use assert for broken assumptions and impossible states.

assert provides an always-checked statement intrinsic for invariants.

The accepted end-state contract uses one required condition and one optional message:

assert |condition Bool, message String? = none|

This is compiler-owned metadata, not an importable or shadowable Moth function. The current Alpha compiler carries the message as a typed optional value through AST and HIR. JavaScript and HTML evaluate reachable runtime message construction lazily on the failure edge; HTML-Wasm currently rejects reachable runtime construction while accepting default and fully folded forms. The rules below describe the accepted end state and the explicit target gap.

assert(index < items.length())
assert(index < items.length(), "index must be in bounds")
assert(false, "unimplemented backend path")

Accepted forms

assert(ready)
assert(ready, "must be ready")
assert(condition = ready)
assert(ready, message = "must be ready")
assert(message = "must be ready", condition = ready)
assert(condition = ready, message = none)
  • The condition must resolve to Bool.
  • The condition is required.
  • The message resolves to String? and defaults to none.
  • A String promotes to String? through ordinary contextual coercion.
  • none selects the default failure text, "assertion failed".
  • A present message may be an ordinary infallible expression, including a local string, runtime template or infallible string-producing call.
  • Moth does not implicitly stringify other value types for an assertion message.
  • Named and positional arguments use the ordinary call rules. Positional arguments must come first, names must be condition or message, and a parameter cannot be supplied twice.
  • Mutable access markers are invalid for both parameters.
  • Parentheses are required.
  • The shared call-argument grammar owns trailing-comma behaviour.

The message is type checked, generic-inferred, evidence-checked and fully normalized, including nested templates, even when the condition is compile-time true. The normalized inactive message is then replaced with typed none before the AST/HIR boundary, so it publishes no TIR, runtime handoff, reactive metadata, generated-function request or downstream executable fact. An active message is evaluated only on the failure edge. A template-backed message is read as one snapshot at failure and does not create an assertion sink or live subscription. Message construction cannot escape its evaluation through !, ?, return, break or continue. Handle fallible work before the assertion and pass its infallible value instead.

Failure behaviour

  • Assertions are checked in development and release builds.
  • Failure is unrecoverable in Moth source.
  • Failure does not produce Error!.
  • Postfix ! is invalid.
  • catch is invalid.
  • Expected runtime failures should use a typed error path instead.

Statement-only contract

assert:

  • cannot be assigned
  • cannot be passed as an argument
  • cannot be imported or aliased
  • cannot appear in template interpolation
  • does not produce a value

assert(false) and assert(false, "message") are statically terminal. assert(false, runtime_message) is also statically terminal after the failure-edge message is lowered.

They may end a function path that otherwise requires a value.

A dynamic assert(condition) is not statically terminal because the condition may succeed.