Learn Dodo Dodo 0.1.4

Patterns and Results

Handle errors with Result, propagate with ?, unwrap with !, and unpack values using exhaustive patterns.

On this page

Dodo represents a recoverable failure as an ordinary value: Result<T, E>, also written T!E. T is the success type and E is the error type. A Result contains exactly one of ok(value) or err(error). Error types are usually enums, but do not need to inherit from a special error class.

Patterns describe which alternative you expect and which values you want to bind. They are used in match, if let, and destructuring let. This chapter starts with error handling, then explains the full pattern vocabulary.

Return and match a Result

package main

enum ParseError { NotADigit }

fn digit(byte: u8) -> u32!ParseError {
    if byte < b'0' || byte > b'9' {
        return err(ParseError.NotADigit)
    }
    ok((byte - b'0') as u32)
}

fn main() {
    match digit(b'7') {
        ok(value) => { core.assert_eq(value, 7u32) },
        err(ParseError.NotADigit) => { core.assert(false) },
    }
    match digit(b'x') {
        ok(_) => { core.assert(false) },
        err(_) => {},
    }
}

The first match extracts the success value. The second explicitly accepts an error, so its empty error arm is intentional handling. match has no fallthrough and must cover every possible input. Arms can contain an expression or a block. The success and error constructors use the expected return type to infer the complete Result type; a standalone local may need an annotation such as outcome: u32!ParseError = ok(7).

Propagate with ?

Postfix ? obtains the success value or immediately returns the error from the enclosing function. It lets the caller decide what to do about failure:

package main

enum ParseError { NotADigit }

fn digit(byte: u8) -> u32!ParseError {
    if byte < b'0' || byte > b'9' {
        return err(ParseError.NotADigit)
    }
    ok((byte - b'0') as u32)
}

fn pair(high: u8, low: u8) -> u32!ParseError {
    let tens = digit(high)?
    let units = digit(low)?
    ok(tens * 10 + units)
}

fn main() {
    match pair(b'4', b'2') {
        ok(value) => { core.assert_eq(value, 42u32) },
        err(_) => { core.assert(false) },
    }
}

If the first digit fails, the second call never executes. Locals already created receive normal cleanup. The enclosing function must return a Result with the same error type; Dodo does not insert error conversions. To translate errors, match the original error and construct a new error explicitly.

A fallible operation with no success payload returns void!E and succeeds with ok(). operation()? performs it and continues without binding a value. ? applies to Results, not to Options.

Handle every Result

A Result must be forwarded, propagated with ?, unwrapped with !, or matched with explicit ok and err arms. Binding it and leaving scope, overwriting it unhandled, assigning it to _, or passing it to core.drop is rejected.

Intent Write
Let the caller handle the same Result return operation()
Continue on success and return failure value := operation()?
Recover locally or translate errors match operation() { ok(value) => ..., err(error) => ... }
Treat failure as a terminating panic value := operation()!

Moving a Result into a new binding transfers the obligation; it does not handle it. This also applies to nested Results in structs, arrays, enums, and Options. A wildcard cannot silently discard a pending nested Result. A plain Option<T> does not carry a mandatory handling obligation unless its payload contains one.

After matching by reference, replacing the whole owned binding creates a fresh handling obligation. Assignments of Result-containing values through a field, index, or reference are unsupported, including writes through local aliases: the checker cannot transfer their handling state to the storage owner. Matching plain payloads through &mut still permits mutation of non-Result data. The container element design explains why storing and destroying pending Results needs additional checker support.

Unwrap or panic with !

Postfix ! evaluates a Result once and produces its success value. On error it panics at the expression’s source location using the configured panic handler. It works in any function, including fn main():

package main

import "std/console"

fn main() {
    console.println("Hello, world!")!
}

? instead returns the error from the enclosing function, which must return a Result with the same error type. ! never propagates: panic terminates execution without unwinding or running destructors. On success, ! consumes the Result and preserves the payload’s ownership and borrow dependencies. A nested Result still needs handling. A void success payload produces no value.

Postfix ! has the same precedence as ? and supports chaining, for example nested()!! or read()!.field when the payload is a reference. Bind an owned array or struct payload to a local before indexing or accessing its fields. Prefix !flag remains Boolean negation; T!E remains Result type shorthand.

Match patterns and guards

The following program combines integer ranges, alternatives, a binding with a guard, and a final wildcard:

package main

fn category(value: u32) -> u32 {
    match value {
        0..=9 => 1,
        10 | 20 => 2,
        number if number < 100 => 3,
        _ => 4,
    }
}

fn main() {
    core.assert_eq(category(7), 1u32)
    core.assert_eq(category(20), 2u32)
    core.assert_eq(category(42), 3u32)
    core.assert_eq(category(100), 4u32)
}
Pattern Meaning
value Bind the matched value.
_ Ignore a value, subject to Result handling rules.
true, false, 42, b'A' Match a Boolean or integer literal.
0..10, b'a'..=b'z' Match an exclusive or inclusive integer literal range.
some(value), none, ok(value), err(error) Match built-in tagged alternatives.
Command.Add(value) Match a user-defined enum variant and its payload.
Point { x, y: vertical } Bind struct fields, optionally renaming them.
Point { x, .. } Bind selected fields and ignore the remainder.
`pattern_a pattern_b`

Range endpoints must be integer literals, fit the input type, and form a nonempty ordered range. Strings, floats, tuple patterns, and array/slice destructuring patterns are unavailable. Enum qualifiers can be omitted when the input type identifies the variant, but qualified names are often clearer.

Patterns nest recursively, as in some(some(value)) or a struct field containing an enum pattern. Struct patterns list every field or use .. for the remainder. Alternatives must introduce the same bindings with compatible types and borrow modes. Pattern tests and guards run in source order, retrying alternatives after a false guard. Ownership transfers and destruction occur only after the guard succeeds. The current checker conservatively rejects moves of non-copy values, assignment, and mutable borrows anywhere in a guard, including operations on unrelated local storage. Shared borrows and observer calls are permitted. Guarded arms do not count toward coverage. Existing match-arm bindings remain mutable locals; names introduced by let and if let are immutable bindings that retain the permissions of any references they hold. Exhaustiveness checks preserve correlations between nested fields and partition integer ranges by their endpoints. Each pattern may expand to at most 4,096 alternatives; coverage checking is limited to 131,072 work units. Excessive patterns produce a diagnostic.

Conditional and destructuring bindings

Use if let when you want to act only on one ordinary optional state:

package main

fn main() {
    optional := some(42i32)
    if let some(value) = optional {
        core.assert_eq(value, 42i32)
    } else {
        core.assert(false)
    }
}

Use an early-exit let to keep the successful path at the surrounding indentation:

package main

struct Reading { value: Option<i32>, valid: bool }

fn usable(reading: Reading) -> i32 {
    let Reading { value: some(value), valid: true } = reading else {
        return 0
    }
    value
}

fn main() {
    core.assert_eq(usable(Reading { value: some(42i32), valid: true }), 42i32)
    core.assert_eq(usable(Reading { value: none, valid: true }), 0i32)
}

The else must leave the current path: use return, break, continue, or a nonterminating loop as appropriate. If the pattern cannot fail, no else is needed, for example let Point { x, y } = point. Names in an if let exist only inside its success block; names in a destructuring let are available afterward.

Both forms introduce immutable bindings and consume owned scrutinees on success or failure and borrow reference scrutinees. Conditional patterns must cover every state whose active payload contains a Result, including borrowed values. For example, some(result) may match Option<Result<T, E>> if the bound result is handled; its unmatched none path has no obligation. Success-only ok patterns and ignored nested Results are rejected. Borrowed patterns preserve shared/mutable permissions recursively, including separate loans for disjoint struct fields; owned patterns cannot destructure structs with custom drop.

Match by reference to keep the owner

Matching an owned enum, struct, Option, or Result consumes it. Match &value to inspect it without taking ownership, or &mut value to borrow its payloads for mutation:

package main

fn main() {
    optional := some(40i32)
    match &mut optional {
        some(value) => { *value += 2 },
        none => {},
    }
    match &optional {
        some(value) => { core.assert_eq(*value, 42i32) },
        none => { core.assert(false) },
    }
}

The pattern syntax stays the same; the scrutinee’s shared or mutable reference determines the bindings’ access. Permissions propagate recursively through nested payloads. An ignored owned payload is destroyed exactly once after its arm is selected. Guards run before ownership transfers, so a failed guard cannot consume the value needed by later arms.

See ownership for lifetime and partial-move restrictions, and the repository’s examples/patterns.dodo and examples/hex.dodo for larger examples with nested patterns and parsing.

Type to search all documentation.

Keyboard shortcuts

Search documentation
Ctrl K or /
Move through results
↑ ↓
Open selected result
Enter
Close a dialog
Esc
Show these shortcuts
?