Error Handling
Control Flow introduced the mechanics of try/catch. This page goes further into what you can actually throw, how errors move up the call stack, and when reaching for try/catch is the wrong tool.
Throwing More Than Strings
throw(value) accepts any Stof value — catch (name: type) tries to cast the caught value to match, and a union or unknown catch type accepts more than one shape:
#[main]
fn main() {
try { throw("CustomError"); }
catch (error: str) {
switch (error) {
case "CustomError": pln("handled: custom error");
default: pln("unhandled error type");
}
}
try { throw(100); }
catch (error: int | str) { pln("caught:", error); }
}Throwing a Handler
Because a function is just another value, you can throw one — letting the catch site decide how to recover, rather than hardcoding that logic at the throw:
#[main]
fn main() {
let func = () => { pln("recovering via a custom handler"); };
try { throw(func); }
catch (handler: fn) { handler(); }
}Propagation
An uncaught throw bubbles up through however many function calls are on the stack, until something catches it — or nothing does, and the document run fails:
fn inner() {
throw("failed deep inside");
}
fn middle() {
self.inner();
}
#[main]
fn main() {
try { self.middle(); }
catch (msg: str) { pln("caught at the top:", msg); }
}Neither inner() nor middle() has a try/catch of its own — the error just keeps propagating until main()'s does.
Rethrowing
Catch, adjust, and throw again — the outer catch gets whatever the inner one threw:
#[main]
fn main() {
try {
try { throw('hello'); }
catch (e: str) { throw(e + ', world'); }
} catch (e: str) {
pln('final:', e);
}
}