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:

syntax
try {
  // code that may fail
} catch(e) {
  // e is the thrown value: a Str or any Turn value
}
basic_error.tn
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:

throw_example.tn
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/catch handles 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:

Exit Signal Propagation

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:

  1. 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.
  2. The VM generates a ProcessExit signal. The signal includes the process's Pid and the exit reason.
  3. All linked processes receive the signal. Processes created with spawn_link receive the exit signal as a message in their mailbox.
  4. Supervisors restart or escalate. A supervisor is simply a Turn process that spawns children with spawn_link, then loops on receive to handle exit signals. There is no special supervisor syntax. The pattern emerges naturally from the language primitives.
supervisor.tn
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.


Next Steps