defer takes one statement and moves its execution to the end of the enclosing scope. You read several defers top-to-bottom as you write them; at scope exit they fire bottom-to-top. Step through it and watch the order flip.
defer println("defer A") defer println("defer B") defer println("defer C") println("after defers")
Why bottom-to-top is the right default: registration order is usually acquire order — you grab A, then B (which leans on A), then C (which leans on B). The matching teardown has to run inside-out: release C first, then B, then A. LIFO firing is exactly that inside-out unwind, for free, just from the order you wrote the defers in. after defers prints first because it is ordinary flow that runs while the scope is still open — the deferred lines are held back until the scope closes.
Level 1 was what it is. Level 2 is why it earns a keyword: a defer fires on every exit path, so the teardown gets written once, right next to the setup — and cannot be forgotten on any branch out.
Take a proc with three ways out: two early returns and falling off the bottom. You want a cleanup — close a handle, release a lock, print a timing report — to run on all three. Write it once as a defer at the top, next to where the work begins. Pick a path and watch the same deferred line run no matter how you leave:
The point — cleanup lives next to acquisition. The whole reason to spend a keyword here is where the teardown sits in the source. You write acquire, then defer release on the very next line, and the pair sits together at the top of the scope. The release executes at the bottom, on every path, but the code for it never drifts away from the thing it tears down.
The counterfactual: if a proc had exactly one way out — a single straight-line body with no early returns — you wouldn't need a defer for it; you'd just write the cleanup as the last line. Defer earns its keep precisely when there are multiple exit paths, because that's when "put the cleanup before every return" turns into N copies of the same teardown that all have to stay in sync.
if path == 1 { release() // copy 1 return false } if path == 2 { release() // copy 2 — easy to forget return false } release() // copy 3 return true
Three copies of the same teardown, one per exit. Add a fourth early return later and you must remember the fourth release() — miss it and a handle leaks or a lock is held forever. The defer collapses all of it to one line that the compiler emits at every exit for you, so "did I clean up on this branch?" stops being a question you can get wrong.
Level 2 sold you on the defer. Level 3 is the catch that surprises people: a defer is a recipe to run later, not a snapshot taken now. It reads the current value of every variable it names at the moment it fires — which is scope exit, not where you wrote it.
Register defer fmt.println(y) while y == 10, then reassign y twice. When the defer fires at scope exit, which y does it print — 10, or the latest? Step it:
// y registered at 10, reassigned to 50 then 99 y := 10 defer fmt.println("y at fire time =", y) y = 50 y = 99 // scope exits here — the deferred print runs NOW and reads y
What is actually frozen: the statement — the recipe "go read y and print it." The values it touches are read when the recipe runs. So the defer observes whatever those variables hold at scope exit, after every reassignment the body made. (The two defers in that output, for n and y, also fire LIFO: n's defer was registered second, so it prints first.)
The fix when you need the old value: if you want the value as of registration, capture it yourself — save it into a fresh variable first and let the defer name that. The restore-a-global pattern leans on exactly this: old := g_flag; g_flag = true; defer g_flag = old. The defer reads old at fire time, but old never changed after you saved it, so you get the snapshot — and you rewind to the value that was actually there, not a hardcoded default.
The last level is what emerges from one tight definition — "run this statement at the end of its own scope, after the value is handed back." Two guarantees fall out of it that you never had to ask for.
Guarantee one — the loop body is a scope, so a defer in it fires per iteration. Not queued up to fire all at once later: the loop body opens and closes its scope on every pass, so the deferred line runs at the end of each iteration, interleaved with the work. If you defer a "close this" inside a loop over many handles, each handle is closed before the next one opens — only one is ever live at a time.
Guarantee two — a defer runs after the value is returned, so it can't change what comes back — and the compiler proves it. Because firing happens after the return value is handed to the caller, a defer that tries to assign a named return value would be writing to something already gone. Rather than let you write code that looks like it edits the result, Odin rejects it at compile time:
foo :: proc() -> (n: int) { defer { n = 456 // looks like it sets the return value... } n = 123 return } main.odin(10:3) Error: Assignments to named return values within 'defer' will not affect the value that is returned
The emergent payoff: you never reason about "does this defer fire? on which path? in what order? before or after the return?" — the one rule answers all of it, and the compiler closes the two doors where the answer could bite you (a defer can't smuggle a value into the return, and return itself can't appear inside a deferred block: 'return' cannot be used within a defer statement). The keyword stays a one-line guarantee instead of a behavior you have to keep a model of in your head.
That's the arc: L1 a defer moves a statement to scope exit, firing LIFO → L2 it fires on every exit path, so cleanup sits next to acquisition and is written once → L3 it's a recipe, not a snapshot — it reads live values at fire time → L4 from "run at scope exit, after the return" emerge per-iteration loop firing and a compiler-proven can't-touch-the-return.