Skip to main content

Error Handling

BT represents failure with a first-class error object. A fallible operation produces either its declared success value or an error. Fallible operations include calls to fallible functions, join, channel operations, a map index read (m[k] fails on a missing key), and reading or writing a field through an entity component handle (the entity may be gone).

Every fallible operation must be dealt with in one of two ways:

  • Add ? to propagate the error out of the current fallible function or tree.
  • Add !! { ... } to handle the error locally.

BT has no try/catch syntax.

The error object​

Every error has two read-only fields:

FieldTypeMeaning
codeintA program- or API-defined numeric code
messagestringA human-readable explanation

Create and throw an error with new error(code, message):

fn validate_count(int count) -> void {
if (count < 0) {
throw new error(1, "count must not be negative");
}
}

Libraries should document the codes they produce. Decide on code; use message for diagnostics.

Propagating with ?​

Postfix ? returns from the current function when the operation fails. On success, the expression evaluates to its normal result.

include "buffer";

fn build_greeting(string name) -> buffer {
buffer data = buffer_with_capacity(128)?;
data.append_string("Hello, ")?;
data.append_string(name)?;
return data;
}

? is valid in an ordinary fn or a tree, not in an infallible fn.

Handling with !!​

Attach !! and a block to a fallible call or map index. The block runs only if the operation fails. Inside it, the implicit local err holds the error.

file_read_text("settings.txt") !! {
println(err.message);
file_write_text("settings.txt", "volume=80\n") !! { };
};

On success the block is skipped. The handled expression has type void, so the successful value is dropped. Reaching the end of the block recovers, and execution continues after the statement. The final ; ends the statement.

!! cannot follow other expressions, such as join task. Guard a block instead (below), or propagate with ?.

Rethrowing​

A handler may rethrow err or throw a different error:

fn initialize() -> void {
file_read_text("save.dat") !! {
println(err.message);
throw err;
};
}

fn initialize_strictly() -> void {
file_read_text("save.dat") !! {
throw new error(100, "initialization failed");
};
}

A synchronous handler that throws must be inside a fallible function or tree.

Guarding a block​

!! also attaches to a block. Every failure inside the block goes to the handler, with err bound:

{
string text = file_read_text("level.txt")?;
int level = int_from_string(text)?;
println("starting level " + int_to_string(level));
} !! {
println("load failed: " + err.message);
};

A guarded block works inside an infallible fn, because the handler absorbs every failure that could escape.

Errors from concurrent work​

join is fallible because the task may have failed:

fn load_both() -> int {
task<buffer> first = branch file_read_bytes("first.dat");
task<buffer> second = branch file_read_bytes("second.dat");

buffer a = (join first)?;
buffer b = (join second)?;
return a.size() + b.size();
}

A handler attached to a branched call runs as part of the branched work. It consumes the success value or recovers, so the result is task<void>:

task<void> optional_load = branch file_read_bytes("bonus-level.dat") !! {
println(err.message);
};

(join optional_load)?;

A rethrow from that handler fails the child task.

Cleaning up with &&​

A trailing && { ... } defers cleanup. Its block runs when the guarded part's scope exits by any path: falling off the end, return, break, continue, or a failure unwinding through it.

include "sync";

fn record_score(Mutex lock, array<int> scores) -> void {
lock.lock()? && { lock.unlock(); };
string text = file_read_text("score.txt")?; // if this fails, the unlock still happens
scores.push(int_from_string(text)?);
} // ...and it happens here otherwise

&& on a statement defers to the enclosing block, as above. && on a block scopes the cleanup to that block:

{ println("work"); } && { println("cleanup"); };
println("after");
// -> work, cleanup, after

Several defers in one scope run innermost-first. A deferred block must finish normally: ?, throw, return, break, and continue inside it are compile errors. Cleanup therefore cannot replace or swallow a failure in flight. Handle failures inside it instead:

file_write_text("autosave.txt", "level=3\n")? && {
file_remove("autosave.tmp") !! { };
};

Defers do not run when a trap, such as division by zero, kills the task. A trap is a fatal fault, not an error you can handle.

&& is a guard only when { follows it immediately; a && b is still logical AND.

Chaining​

!! and && chain left to right, each wrapping everything to its left. The order decides whether cleanup runs before or after the handler:

{ A } && { D } !! { H }; // A fails -> D -> H (H sees the cleaned state)
{ A } !! { H } && { D }; // A fails -> H -> D (cleanup outside the handler)

Choosing a style​

Use ? for failures the current function cannot resolve. Use !! when the local context can retry, substitute a fallback, log and continue, or translate the error. An unused success value is no reason to handle an error; file_read_text(path)?; propagates failure and drops the value.