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:
| Field | Type | Meaning |
|---|---|---|
code | int | A program- or API-defined numeric code |
message | string | A 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.