C+
Ownership & safety · View as Markdown

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;
}

The standard library does not trap

As of v0.0.26, every fallible standard-library operation returns its failure instead of trapping the program, so you decide what a failure does. Fallible operations return one of:

  • Status — for a mutation whose question is just "did it succeed?": Ok, OutOfMemory, OutOfBounds, InvalidInput, or Shared. On any non-Ok result the operation made no change and the receiver stays valid.
  • Option[T] — for a lookup that may be absent (a bounds-checked at, a missing key).
  • Result[T, E] — for an operation that yields a value or a typed error.
import "stdlib/status" as status;

let s: status::Status = set.insert(name);   // OutOfMemory, not a trap
guard let status::Status::Ok = s else { return handle_failure(); };

Nothing in the standard library aborts on a recoverable condition, so a program ends only where you write it to.

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.