C+
Ownership & safety · v0.0.13

Error handling without exceptions

There is no try, no catch, no throw, and no ? operator. A fallible function returns a tagged-union value and the caller matches on it. The control flow you see is the control flow that runs.

Define your own result type

enum ParseResult {
    Ok(i32),
    BadInput,
    Overflow,
}

fn parse(s: str) -> ParseResult { ... }

The verbose form: explicit match

match is exhaustive, so you cannot forget a case:

fn parse_or_zero(s: str) -> i32 {
    return match parse(s) {
        ParseResult::Ok(v)       => v,
        ParseResult::BadInput    => 0 -% 1,
        ParseResult::Overflow    => 0 -% 2,
    };
}

The readable form: guard let

guard let binds the happy-path value and forces you to handle the failure by leaving the scope. The rest of the function then reads straight through:

fn handle(s: str) -> i32 {
    guard let ParseResult::Ok(v) = parse(s) else { return 0 -% 1; };
    return v +% 100;
}

Generic Result and Option from the stdlib

For the common shapes, the standard library provides generic types:

import "stdlib/result" as result;
import "stdlib/option" as option;

fn maybe_lookup(k: str) -> option::Option[i32] {
    if k == "answer" { return option::Option[i32]::Some(42); }
    return option::Option[i32]::None;
}

There is no ? propagation operator and no !T magic. The control-flow primitives plus guard let give you the same ergonomics with full locality: a reader never has to look anywhere but the function in front of them to see how an error travels.