Error Handling
Turn provides two complementary error handling mechanisms: structured try/catch/throw for synchronous failures, and process exit signal supervision for actor-level failures. Both are designed to make failure explicit, visible, and structurally handled. Failures are never silently swallowed.
try / catch / throw
Turn has full try/catch/throw support. Use it for localised error handling within a process:
try {
// code that may fail
} catch(e) {
// e is the thrown value: a Str or any Turn value
}let net = use "std/net";
let json = use "std/json";
try {
let id = grant identity::network("public");
let raw = net.get({ "url": "https://api.example.com/data", "identity": id });
let data = json.parse(raw);
call("echo", "Fetched: " + data["name"]);
} catch(e) {
call("echo", "Request failed: " + e);
}You can throw any Turn value:
let fs = use "std/fs";
let id = grant identity::filesystem("local");
let result = fs.read({ "path": "/data/config.json", "identity": id });
if result == null {
throw "Config file not found";
}
call("echo", "Config loaded.");NOTE
throw unwinds to the nearest enclosing catch. If no catch exists in the current process, the throw becomes an unhandled error and terminates the process with an exit signal.
Philosophy: Explicit Failure as Signal
In agentic software, LLM inference failures, HTTP timeouts, and schema coercion errors are expected outcomes, not exceptional edge cases. Turn's error model is built to make failure explicit at every level:
try/catchhandles synchronous, recoverable failures within a process- Process exit signals handle catastrophic failures across the actor boundary
- No failure mode is hidden: every unhandled error becomes a structured, observable exit signal
Process Exit Signals
When a process fails due to an unhandled throw, token budget exhaustion, or a native tool error, the VM does not crash the entire program. It follows an Erlang-style "let it crash" philosophy:
When a process terminates abnormally, the VM generates a ProcessExit signal containing the exit reason. Every process linked to the failed process via spawn_link receives this signal in its mailbox. Supervisors can match on exit reasons and decide whether to restart the child, escalate the failure, or compensate.
The sequence of events is:
- The process encounters an unhandled error. This could be an unhandled
throw, exhaustion of the token budget, or a panic in a native tool handler. - The VM generates a
ProcessExitsignal. The signal includes the process'sPidand the exit reason. - All linked processes receive the signal. Processes created with
spawn_linkreceive the exit signal as a message in their mailbox. - Supervisors restart or escalate. A supervisor is simply a Turn process that spawns children with
spawn_link, then loops onreceiveto handle exit signals. There is no special supervisor syntax. The pattern emerges naturally from the language primitives.
let net = use "std/net";
struct Task { name: Str, payload: Str };
let worker_pid = spawn_link turn(t: Task) {
let id = grant identity::network("public");
let result = net.post({ "url": "https://api.example.com/run", "body": t.payload, "identity": id });
return result;
};
send worker_pid, Task { name: "analysis", payload: "Q4 data" };
let msg = receive;
if msg["type"] == "exit" {
if msg["reason"] == "normal" {
call("echo", "Worker completed: " + msg["result"]);
} else {
call("echo", "Worker failed: " + msg["reason"]);
}
}This design isolates failures to individual processes. A crashed agent does not bring down the entire system.