Odin · error handling

or_return, four levels deep

An error in Odin is an ordinary value handed back out the front door of a procedure — not a thing that jumps out sideways. A proc that can fail widens its return from -> f64 to -> (f64, Math_Error): a second slot riding alongside the result, holding a plain enum. The caller reads that slot and branches. Pick an input and watch the second slot fill.

Outputs below are real, captured from a compiled Odin program (claim explicit-branch).
return slot 0  —  the result
type f64
return slot 1  —  the error
type Math_Error (an enum; .None == "no error")

The error type

Math_Error :: enum {
  None,
  Divide_By_Zero,
  Negative_Sqrt,
}

The first variant, None, is the zero value and means "no error." A fresh Math_Error starts at .None — success is the default state.

What you're seeing

errors travel the same path as results There is no second, invisible exit from divide. A failure leaves through the very same return the success does — only the second slot differs. To know every way a proc can exit, you read its return signature: (f64, Math_Error) says "this returns an f64 and may report a Math_Error," full stop. The failure modes are written into the type.

Level 1 was the shape: (result, err). The friction is at the call site — every single call to a fallible proc tempts the same five-line ritual: take both values, test the error, return it if set. or_return is that ritual, collapsed into one token. And it is only sugar — toggle to see exactly what the compiler expands it into.

what you write  ·  or_return


      

what the compiler expands it to


      
these two procedures are behavior-identical — proven equal output by claim or-return-desugars-equiv

What it really is: or_return evaluates the call, grabs the trailing error, and — if that error is not .None — assigns it to the enclosing proc's own error slot and does a bare return. No value escapes, no special path runs. It is a return statement the compiler typed for you. Hold both pictures at once: the one-token form you read, and the branch it stands for. When something refuses to compile, drop back to the expanded form and the reason is usually staring at you.

The counterfactual: if a proc never fails — its signature is just -> f64, no error slot — there is nothing for or_return to propagate, and you would never reach for it. The operator earns its keep precisely on the chains where every step can fail and you would otherwise write that five-line block over and over.

Level 2 showed one or_return in isolation. Its real power shows in a chain: safe_chain calls divide, then feeds the quotient into safe_sqrt, each guarded by or_return. Whichever step fails first is where the proc exits early, carrying that step's error straight to the caller. Step through three inputs and watch where the chain bails.

The three result lines are real program output (claim chain-trace).

The emergent property: the happy path reads as a straight linedivide then safe_sqrt then return, with no error-handling noise wedged between the steps. Yet a failure in any step still short-circuits cleanly to the caller, carrying the right error. One enum, written once into the signature, ferries information from two different failure sites upward — and the code that does it has no visible if err at all. The boring case stopped crowding the page; the interesting case still can't slip past you.


  

Because or_return is literally a bare return (Level 2), two rules fall straight out of that fact — and a second operator, or_else, fills the other half of the design. This is where errors-as-values pays off: the things you must not do are caught at build time, not discovered in a running frame.

The two operators

x := f() or_return
Propagate upward. On failure, bail out of this proc, sending the error to my caller. The recover-it-later move. Needs the enclosing proc to have an error return.
x := f() or_else d
Recover locally. On failure, substitute the default d and carry on right here — the error never leaves this line. You get back a single value; the error tail is consumed.

or_else in action — the default lands only when the call failed:



    

Three things the compiler refuses

Each of these is a real build error — pick one:


    

or_return needs named returns — here's why, exactly or_return expands to a bare return — a return with no values listed. A bare return only knows what to send back when the return slots have names to send back: -> (result: f64, err: Math_Error). Drop the names to -> (f64, Math_Error) and the bare return has nothing to refer to, so the build stops. Single-return procs are fine unnamed; the rule bites only with two or more returns. The desugaring isn't a metaphor — this restriction is the desugaring showing through.

That's the arc: L1 an error is a plain value in a second return slot, read and branched on → L2 or_return is one token for the test-and-bail branch, nothing more → L3 chained, it makes the happy path a straight line while still short-circuiting any failure → L4 being a bare return forces named slots, forbids silently dropping the error, and pairs with or_else for local recovery — all checked at build time.

probes reproduce with odin run · outputs, the desugaring equivalence & the L4 errors are real compiler output (claims/lessons/11-error-handling-or-return)