Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Errors and optionality

rut has no exceptions you catch, no Result enum, and no null. Two mechanisms cover everything:

  • Absence is nil on a nullable ?T.
  • Failure is a value in the second slot of a pair — the (T, err) answer channel.
  • Bugs — broken contracts — trap loudly and immediately (panic, assert, a nil deref, an out-of-bounds index).

The design background lives in everything is a value.

?T and nil: absence

?T is a nil-able cell. A plain T always holds a value; ?T is that value or nil:

use ink::{ Logger };

struct Node {
    value: i32;
    left: ?Node;      // one word — recursive shapes are legal
    right: ?Node;
}

pub fn main() {
    let log = Logger.new("nodes");
    let n = Node { value: 1, left: nil, right: nil };
    log.info(f"left is nil: {n.left == nil}");
}
left is nil: true

Lookups answer ?T: a hit is the value, a miss is nil. Deref is automatic at every use — p.x, p[i], for (let x of p), arithmetic — and dereferencing a nil traps NilDeref, so guard first:

use nmapset::{ HashMap };
use ink::{ Logger };

pub fn main() {
    let log = Logger.new("optional");
    let mut scores: HashMap<str, i32> = HashMap.new();
    scores.put("rut", 41);
    let mut n = 0;
    let hit = scores.get("rut");      // ?i32
    if (hit != nil) {
        n = hit + 1;                  // a hit auto-unwraps for `+`
    }
    log.info(f"n={n}");
}
n=42

p == nil compares against the null. There is no flow-typing: after the guard, hand the value to a typed binding when you want a plain T:

let err = map_response(resp, "no such repo or ref", "data.jsdelivr.com");
if (err != nil) {
    let why: str = err;           // the plain str behind the nullable
    eprint(why);
    return 1;
}

nil in an expected-?T position just works: let p: ?Node = nil;, or a left: nil field in a literal.

The (T, err) answer channel

Functions that can fail return a pair. The convention is uniform:

  • empty err + a value — success;
  • empty err + nil — a legitimate “not found”;
  • non-empty err — failure.

The simplest err is a str message (empty string means success):

fn hex_dec(s: str) -> (bytes, str) {
    if (s.len() % 2 != 0) {
        return (bytes.zeroed(0), "hex: odd-length input");
    }
    // ... decode ...
    return (out.freeze(), "");
}

Nullable halves make both slots explicit — this is the standard library’s own shape, (?T, ?E):

// json's entry points
fn decodeJson<T requires JsonDeserialize>(s: str) -> (?T, ?DecodeJsonError);
fn encodeJson<T requires JsonSerialize>(v: T) -> (?str, ?EncodeJsonError);

The caller destructures and checks the err half first:

use json::{ decodeJson };
use ink::{ Logger };

pub fn main() {
    let log = Logger.new("json");
    let (n, e) = decodeJson<i64>("42");
    if (e == nil) {
        log.info(f"n={n}");
    } else {
        let why = e;
        log.info(f"decode failed at {why.at}");
    }
}
n=42

Integer arithmetic’s checked ladder uses the same idea in miniature: x.checked_add(y) answers (T, bool) where .1 is false exactly when the result escaped the width — see values and variables.

Building an error type

For errors a caller might want to branch on, pair a payloadless enum (the kind) with a fixed-field struct (the details). That is the shape json uses:

pub enum DecodeErrorKind { Unexpected, Truncated, InvalidUtf8, WrongType, Depth, Trailing }

pub struct DecodeJsonError {
    kind: DecodeErrorKind;
    at: i64;        // codepoint offset in the input
    got: str;       // the offending text
    expected: str;  // what the schema asked for here
}

The caller picks: branch on kind, or just render the fields:

use json::{ decodeJson };
use ink::{ Logger };

pub fn main() {
    let log = Logger.new("json");
    let (bad, be) = decodeJson<i64>("[1,2,3]");
    if (be != nil) {
        let why = be;
        // rejected at 0: got '[', wanted an i64
        log.info(f"rejected at {why.at}: got '{why.got}', wanted {why.expected}");
    }
}
rejected at 0: got '[', wanted an i64

Enums have no payloads — the struct carries the data. This split keeps every error a plain value: copy it, store it in a log, put a location next to it, test it.

panic and assert: for bugs, not for flow

panic("Rect: negative extents");        // abort with a message
assert(total == expected, "checksum");  // abort when the condition is false

Use these when continuing would be a lie: a broken invariant, an impossible state, a caller error that is a programming mistake. Data problems — a bad byte in a file, a missing key, a network error — are values and belong in the answer channel, so callers can recover.

A trap’s message and a stack trace (opt-in via capture_stacktrace()) land in the host’s diagnostics — see diagnostics, traces, and symbolication.

Erasure when you truly need it

When a value must cross a boundary that cannot name its type — host handles, heterogeneous boxes — opaque(v) seals it and opaque.downcast<T>(o) -> ?T recovers it (nil on a wrong type, never a trap):

use ink::{ Logger };

struct Point { x: i32; y: i32 }

pub fn main() {
    let log = Logger.new("opaque");
    let box1 = opaque(Point { x: 1, y: 2 });
    let p = opaque.downcast<Point>(box1);   // ?Point
    when (p != nil) {
        true -> { log.info(f"point {p.x} {p.y}"); },
        else -> { log.info("point: nil"); },
    }
}
point 1 2

x is T probes the box without recovering it. This is rut’s only any-shaped value, and it can do nothing until recovered — see the reference on opaque.

Put it together

use json::{ decodeJson, encodeJson };
use ink::{ Logger };

pub fn main() {
    let log = Logger.new("json");

    // a successful decode: (value, nil)
    let (n, e) = decodeJson<i64>("42");
    if (e == nil) {
        log.info(f"n={n}");
    } else {
        let why = e;
        log.info(f"decode failed at {why.at}: {why.expected}");
    }

    // encode answers the same shape: (?str, ?EncodeJsonError)
    let (s, ee) = encodeJson<[i64]>([1, 2, 3]);
    if (ee == nil) {
        let text = s;
        log.info(f"s={text}");
    } else {
        log.info("encode failed");
    }

    // a bad decode: the (nil, err) half answers the failure
    let (bad, be) = decodeJson<i64>("[1,2,3]");
    if (be == nil) {
        log.info("unexpected success");
    } else {
        let why = be;
        log.info(f"rejected at {why.at}: got '{why.got}', wanted {why.expected}");
    }
}
n=42
s=[1,2,3]
rejected at 0: got '[', wanted an i64

Next: modules and packages.