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.
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 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 emergent property: the happy path reads as a straight line — divide 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.
or_else in action — the default lands only when the call failed:
Each of these is a real build error — pick one:
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.