Error Handling

Error Handling

Returned error values

Prefer standard-library result enums when using the standard library. std.AccessResult<T> is the precise result of checked vector, buffer, and string access operations: its variants are Ok(T) and OutOfBounds(IndexOutOfBoundsError). std.Result<T> combines standard error categories for functions that call several APIs. Both are defined in stdlib/result.sun; the compiler does not identify them by name.

_Result<T, E> remains builtin and needs no import. Its Ok payload contains the successful value, and its Err payload contains the concrete error. All these results follow ordinary enum ownership and cleanup rules, storing the active payload inline without allocating exception storage.

/** Describes invalid port numbers. */
enum PortError { OutOfRange(i32) }
 
/** Validates a port without allocating. */
function validate_port(port: i32) _Result<i32, PortError> {
    if (port < 1 or port > 65535) {
        return _Result.Err(PortError.OutOfRange(port));
    }
    return _Result.Ok(port);
}
 
/** Propagates an invalid port to its caller. */
function configure_port(port: i32) _Result<i32, PortError> {
    var valid = try validate_port(port);
    return _Result.Ok(valid);
}

A plain call returns a result for storage, forwarding, or handling with match:

var port = match validate_port(input) {
    _Result.Ok(value) => value,
    _Result.Err(error) => 8080
};

Builtin and standard results cannot be silently discarded, including by storing it in an unused local. Use ignore_error(operation()) to explicitly discard a result; this drops its owned success or error payload.

Prefix try works with any nonempty enum whose first declared variant has zero or one payload and whose remaining variants each have one payload. The first variant represents success: try extracts its payload, or produces void when it has none. Variant names and modules do not affect this rule. Declaration order is therefore part of an enum's public contract.

/** Returns a value or a bounds error. */
enum AccessResult<T> {
    Value(T),
    OutOfBounds(IndexOutOfBoundsError)
}

try evaluates its operand once. For a failure, it moves the payload into a compatible failure variant of the enclosing function's returned enum and cleans up live owners. When both enums are the same type, the original failure variant is preserved. Otherwise, each possible failure must have exactly one destination failure variant with the same payload type. The destination's first variant is never an error destination. Missing or ambiguous conversions are compile errors, even if a particular call happens to return success.

For compatibility with explicit error sums such as _Result<T, ErrorKind>, a payload may also be wrapped in a unique immediate variant of the destination error payload enum when no direct destination matches. This does not search recursively through nested enums.

Calls, indexing, and member access belong to the operand; binary operators do not: try read_value() + 1 means (try read_value()) + 1. In a generic enum, a sole first-variant payload specialized to void becomes a unit success variant.

_Result<void, E> represents success without a value, written _Result.Ok. try operation(); propagates such an operation. A successful reference such as _Result<ref T, E> stays a borrow after propagation; its owner must remain alive.

A fallible constructor can declare init(...) AccessResult<void>. Calling the class returns AccessResult<Class>, so var object = try ClassName(args); propagates a construction error. Any generic result enum can be used when its first type parameter holds the success value and its failure payload types do not depend on that parameter. Additional type parameters are preserved.

Reaching the end of init, return;, and return AccessResult.Ok; all report success. Use return AccessResult.OutOfBounds(error); for failure. The first variant determines success; it need not be named Ok. Each failure variant must have one payload. Every successful path must initialize every field; failure drops initialized fields without calling deinit on the incomplete object. The unsafe _init<T> operation returns the constructor's original status type, such as AccessResult<void>, for explicit handling. Builtin _Result<void, E> constructors remain supported.

Errors may also be borrowed explicitly, including through interface payloads:

/** Borrows errors whose storage belongs to another owner. */
enum BorrowedResult {
    Ok,
    RetryableError(const ref IRetryableError),
    OtherError(const ref IError)
}

The owner must outlive every use of the borrowed result. A function cannot return an error view pointing into its own local storage. Custom enums support both match and prefix try when their variants satisfy the declaration-order rule above.

See typed match bindings for handling several concrete payload types through one interface pattern.

Checked integer arithmetic

checked_div(a, b) and checked_rem(a, b) return _Result<T, ArithmeticError>, where T is the operands' promoted integer type. They evaluate each operand once and return an error for a zero divisor or signed minimum divided by -1 (including remainder).

function quotient(a: i32, b: i32) _Result<i32, ArithmeticError> {
    var value = try checked_div(a, b);
    return _Result.Ok(value);
}

Error messages

IError.code() and IError.message() are const methods. With the standard library, message() returns an owned String copy that may outlive the error. Without the standard library, the existing static_ptr<u8> contract is retained.

Error Types

An error is a class that implements the builtin IError interface:

interface IError {
    const method code() i32;
    const method message() String;
}

Define your own by implementing both methods. Because an error is an ordinary class, it can carry whatever fields and extra methods you need:

class ParseError implements IError {
    var line_: i32;
    init(line: i32) { this.line_ = line; }
    const method code() i32 { return 1; }
    const method message() String { return String("bad input"); }
    method line() i32 { return this.line_; }
}

The standard library (using std;) provides Error(code, message) plus a set of ready-made ones: EmptyError, NotFoundError, IndexOutOfBoundsError, DivisionByZeroError, OverflowError, InvalidArgumentError, IOError, AllocationError and ConversionError. Error accepts its message as a literal or as a String built at runtime, and keeps its own copy.

Standard-library results

Fallible library calls return their concrete error types:

OperationError type
Vec.get, Vec.set, checked buffer indexingIndexOutOfBoundsError
Map.get, Map.removeNotFoundError
LinkedList.pop_front, String.popEmptyError
safe_convertConversionError
JSON readers and parsingJsonError
Protocol-buffer readsProtoDecodeError
File, process, environment, and socket operationsError
Test assertionsstd.test.AssertionError

For a function that returns _Result<void, Error>, standalone propagation needs no grouping parentheses:

try std.io.make_dir(path, 493);
return _Result.Ok;

Handle an error without binding its payload with _Result.Err(_). To inspect several concrete error payloads through one interface, use a typed borrowed arm:

match operation() {
    _Result.Ok(value) => use(value),
    (error: const ref IError) => println(error.message())
};

Typed arms inspect immediate payloads. If E is itself an enum of error classes, first match _Result.Err(error), then match error with the interface arm.

Built-in Arithmetic Errors

ArithmeticError implements IError and is available without an import. checked_div and checked_rem return it on failure; ordinary integer arithmetic traps instead.

/** Divides two integers, returning recoverable arithmetic failures. */
function divide(a: i32, b: i32) _Result<i32, ArithmeticError> {
  return checked_div(a, b);
}
 
/** Handles division by zero through the error interface. */
function main() i32 {
  return match divide(1, 0) {
    _Result.Ok(value) => value,
    (error: const ref IError) => error.code()
  };
}
Failurecode()message()
Zero divisor4integer division by zero
Signed minimum divided by -1, including remainder5integer division overflow

message() returns an owned String with the standard library, retaining the existing static_ptr<u8> contract without it. Prefix try drops live owners before returning a failure to its caller. A trap terminates execution without running cleanup.

Cleanup during propagation

Prefix try returns early on failure and drops live owners before returning. Code before the failing call has run; code after it has not. An owned error moves through _Result.Err without copying its payload or allocating exception storage. A borrowed error never transfers ownership of its referent.

Replacing Exception Syntax

The standard library, TLS APIs, generated protocol-buffer decoders, and test runner use returned results. throws, throw, and block-form try/catch are rejected with migration diagnostics. Declare a result return type, return an error variant, and use try expression for propagation or match for handling.

Builtin _Result<T, E> remains available. Prefer library-defined result enums with concrete owned errors; borrowed interface payloads are supported when their referents outlive the result.