Errors
How things fail in BT, how you must deal with failures, how cleanup runs, and how failures differ from crashes (traps).
include "io";
include "files";
include "string";
object Level { string name; int width; }
fn load_level(string path) -> Level {
string text = file_read_text(path)?; // on failure, pass the error to our caller
int width = int_from_string(text)?;
if (width <= 0) {
throw new error(2, "level has no width: " + path);
}
return Level { name: path, width: width };
}
infallible fn start_game(string path) {
load_level(path) !! { // handle it here
println("could not load " + path + ": " + err.message);
};
}
Failures and traps
| Failure | Trap | |
|---|---|---|
| Caused by | throw, or an operation that can fail (a missing map key, a closed channel, a failing file or engine call) | A bug: integer division by zero, index out of range, using null, bad slice bounds, recursion too deep |
| Carries | an error with a code and message | a numeric code |
| Can you catch it? | Yes, with !! | No |
&& cleanup runs? | Yes | No |
| If nothing handles it | The task fails with that error | The task is stopped |
Treat traps as bugs to fix: check an index or divisor before using it.
The error object
throw new error(404, "no such level: " + name);
errorhas two read-only fields:code(int) andmessage(string).- Create one only with
new error(code, message). - Errors are ordinary values: store, pass, return and rethrow them.
- Pick your own codes. Branch on
codein your logic; usemessagefor logs and display.
Codes produced by the language itself:
| Code | When |
|---|---|
1 | Reading a map key that isn't there (map key not found). |
3 | E(n) when n is no member's value, or T(value) when the value is null or not a T (see Conversions). |
-102 | pop on an empty array, or insert/remove with a bad index (native error). |
-300 | send, receive or select (without else) on a closed channel. |
-305 | An unless channel fired before the join, receive or select it guards finished (see Cancelling and timeouts). |
-410 | Reading or writing a component field whose data no longer exists, for example because the entity was destroyed. |
Library and engine codes are listed with each function in the BT standard library and cTurtle API references.
What can fail
These can fail and must be handled:
- calls to functions and methods declared
fnortree, including standard library and engine functions (onlyinfallible fncannot fail); - calls through a
fn(...)function value; join t;c.send(v)andc.receive();- reading a map:
m[k](unless insideif (m ?? k)); - converting to an enum or downcasting:
E(n),T(value); rows[i];- reading or writing fields of a
component<T>or of another engine-provided struct (what it refers to may no longer exist); - a
selectwithoutelse.
You must handle every failure
Each expression that can fail must be dealt with on the spot, in one of these ways:
?passes the failure on;!! { ... }on the call handles it;- an enclosing
{ ... } !! { ... }block handles it; branchmoves it into the new task (wherejoinsees it).
Otherwise the program won't compile:
fallible expression must be handled with '!!' or propagated with '?' (the
message may say initializer, argument, condition and so on instead of
expression). This applies even when you ignore the result: write
file_remove(path)?;.
Inside a { ... } !! { ... } block, an expression without ? is also
accepted; a failure goes to the handler.
Passing failures on: ?
fn read_header(string path) -> string {
buffer data = file_read_bytes(path)?;
return data.range_to_string(0, 16)?;
}
- On success,
expr?gives the value. On failure, it jumps to the nearest enclosing!!handler in the function, or returns from the function with the same error. ?is allowed in functions that can fail, inbranch { }blocks, and inside!!guards. In aninfallible fnoutside those it isan infallible function cannot propagate failure.?on something that cannot fail is an error.?is not allowed inside an&&cleanup block.
Handling failures: !!
file_remove("cache.bin") !! {
println("no cache to remove: " + err.message);
};
{
string text = file_read_text("settings.txt")?;
println("volume: " + int_to_string(int_from_string(text)?));
} !! {
println("load failed: " + err.message);
};
-
The handler runs only on failure, with
errholding the error. -
When the handler finishes, the program continues after the guarded statement: the failure is dealt with.
-
The handler may also
return,break,continueorthrow. -
A handled call gives no value. To keep a value, assign it inside a guarded block:
int v = 0;{ v = int_from_string(text)?; } !! { v = -1; }; -
!!directly after an expression only works on a call or index that can fail. Forjoin t, guard a block. -
A guarded block deals with every failure inside it, so it can be used in an
infallible fn.
throw
throw new error(1, "count must not be negative");
throw err; // rethrow inside a handler
- The value must be an
error. - Allowed in functions that can fail,
branch { }blocks, and inside!!guards. Rethrowing from a handler in aninfallible fnisan infallible function cannot throw. - Not allowed inside an
&&cleanup block.
Cleanup: &&
Mutex lock = mutex_create()?;
lock.lock()? && { lock.unlock(); };
string text = file_read_text("scores.txt")?; // if this fails, unlock still runs
file_write_text("scores.txt", text + "1200\n")?;
expr && { ... };runs the block when the enclosing block is left.{ ... } && { ... };runs it when the first block is left.- It runs however the block is left: normally, by
return,break,continue, or a failure. It does not run if the task traps. - Several cleanup blocks run in reverse order: last registered, first run.
- A cleanup block must finish normally:
?,throw,return,breakandcontinueinside it are errors (a deferred block must finish normally). Handle failures inside it with!!. - Cleanup never hides a failure that is already on its way out.
Combining && and !!
Guards chain left to right, each wrapping everything to its left:
{ A } && { D } !! { H }; // A fails -> D runs, then H
{ A } !! { H } && { D }; // A fails -> H runs, then D
On a plain statement, && waits for the enclosing block: in
f() && { D } !! { H };, if f fails, H runs at once and D runs when the
enclosing block exits.
Failures in tasks
- A failure that escapes a
branchcall or block fails that task. join ton a failed task fails with the same error, every time.branch f() !! { ... }runs the handler in the new task and givestask<void>.- A failure in a task that nobody joins is lost silently. Add a handler if you need to know about it.
- If the engine cancels a task, joining it fails.
Uncaught failures and traps
| Where | Unhandled failure | Trap |
|---|---|---|
main of a standalone program | Prints BT native entry 'main' failed (failure <code>) and exits with status 1. | Same, with the trap code. |
A task started with branch | Kept in the task; join sees it. | Task stopped; join fails. |
| A tree or callback started by the engine | Reported by the engine (log or console). | Reported by the engine. |
Trap codes you are most likely to see in such messages:
| Code | Cause |
|---|---|
-1 | A fault inside BT itself, not in your code. Please report it. |
-3 | Recursion too deep. |
-4 | Integer division or % by zero. |
-6 | join on a null task. |
-8 | Array or slice index out of range, copy with mismatched lengths, or negative array length. |
-9 | Used a null reference. |
-11 | Added a new key to a map while a for ... in loop was iterating it. |
-102 | Invalid slice bounds, or a negative reserve/resize. |
See also
- Guide: Error handling