Skip to main content

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​

FailureTrap
Caused bythrow, 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
Carriesan error with a code and messagea numeric code
Can you catch it?Yes, with !!No
&& cleanup runs?YesNo
If nothing handles itThe task fails with that errorThe 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);
  • error has two read-only fields: code (int) and message (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 code in your logic; use message for logs and display.

Codes produced by the language itself:

CodeWhen
1Reading a map key that isn't there (map key not found).
3E(n) when n is no member's value, or T(value) when the value is null or not a T (see Conversions).
-102pop on an empty array, or insert/remove with a bad index (native error).
-300send, receive or select (without else) on a closed channel.
-305An unless channel fired before the join, receive or select it guards finished (see Cancelling and timeouts).
-410Reading 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 fn or tree, including standard library and engine functions (only infallible fn cannot fail);
  • calls through a fn(...) function value;
  • join t;
  • c.send(v) and c.receive();
  • reading a map: m[k] (unless inside if (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 select without else.

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;
  • branch moves it into the new task (where join sees 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, in branch { } blocks, and inside !! guards. In an infallible fn outside those it is an 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 err holding the error.

  • When the handler finishes, the program continues after the guarded statement: the failure is dealt with.

  • The handler may also return, break, continue or throw.

  • 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. For join 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 an infallible fn is an 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, break and continue inside 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 branch call or block fails that task.
  • join t on a failed task fails with the same error, every time.
  • branch f() !! { ... } runs the handler in the new task and gives task<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​

WhereUnhandled failureTrap
main of a standalone programPrints BT native entry 'main' failed (failure <code>) and exits with status 1.Same, with the trap code.
A task started with branchKept in the task; join sees it.Task stopped; join fails.
A tree or callback started by the engineReported by the engine (log or console).Reported by the engine.

Trap codes you are most likely to see in such messages:

CodeCause
-1A fault inside BT itself, not in your code. Please report it.
-3Recursion too deep.
-4Integer division or % by zero.
-6join on a null task.
-8Array or slice index out of range, copy with mismatched lengths, or negative array length.
-9Used a null reference.
-11Added a new key to a map while a for ... in loop was iterating it.
-102Invalid slice bounds, or a negative reserve/resize.

See also​