Documentation / Errors, options and assertions

Errors, options and assertions

Missing data, expected failure and a broken invariant are three different problems. Moth gives each one a separate path.

Use options for ordinary absence. Use typed error returns when callers need failure information. Use assertions when continuing would be wrong.

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_empty():
            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_empty():
            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.
  • In the current compiler return! inside a nested block of an error-only function is not yet supported. Use a direct top-level return! or a function that also returns a success value.

    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.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.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"
    

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
    

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 must be a quoted string literal.

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.


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

Accepted forms


    assert(condition)
    assert(condition, "literal message")
    
  • The condition must resolve to Bool.
  • The condition is required.
  • The message is optional.
  • When present the message must be one quoted string literal.
  • Exactly one condition and at most one message are accepted.
  • Named arguments are invalid.
  • Mutable access markers are invalid.
  • Parentheses are required.
  • Trailing comma without a message is invalid.

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.

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

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