Skip to main content

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:

throw-values.stof
#[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); }
}
Output

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:

throw-handler.stof
#[main]
fn main() {
  let func = () => { pln("recovering via a custom handler"); };
  try { throw(func); }
  catch (handler: fn) { handler(); }
}
Output

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:

propagation.stof
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); }
}
Output

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:

rethrow.stof
#[main]
fn main() {
  try {
      try { throw('hello'); }
      catch (e: str) { throw(e + ', world'); }
  } catch (e: str) {
      pln('final:', e);
  }
}
Output