Sunday, August 16, 2026
Why Are Deferred Function Arguments Evaluated Immediately in Go?
Posted by

Because defer stores a call: function plus already-computed arguments. The call runs at return. The expressions in the argument list run now. Closures are the exception: they capture variables and read them when they execute. Mix the two and you will log the wrong err.
The failure
You want the deferred helper to see the error you assign later. It prints "ok".
package main
import (
"errors"
"fmt"
)
func a() {
var err error
defer fmt.Println("logged:", err) // err copied here: still nil
err = errors.New("boom")
}
func main() {
a()
}
Output: logged: <nil>.
Same trap: defer rollback(err) after err is still nil. The pointer-or-closure form would see the later assignment.
defer f() then f = other still runs the first f. A nil f panics at return, not at the defer line.
Why this happens
The spec splits two times:
- Defer statement: evaluate the function value, the receiver, and every argument. Copy those values onto the defer stack.
- Function return: run the stored calls LIFO.
A value argument is a snapshot. A pointer argument is a snapshot of the address; the bytes at that address can change. A closure with no parameters does not snapshot: it names the variable and reads it at step 2.
That is why this pair disagrees:
var err error
defer func() { fmt.Println(err) }() // sees boom
defer func(e error) { fmt.Println(e) }(err) // sees nil
err = errors.New("boom")
defer trace("name")() calls trace immediately (enter log, capture start time) and defers the returned func. Forget the trailing () and you defer trace itself.
The fix
Need the value at defer time? Pass it as an argument.
Need the value at return time? Close over the variable, or pass a pointer:
func a() (err error) {
defer func() {
if err != nil {
fmt.Println("logged:", err)
}
}()
err = errors.New("boom")
return
}
Named results exist so that deferred closures can write err after a return or recover. Anonymous results are already copied; the defer cannot change what the caller gets.
For a loop, defer fmt.Println(i) freezes each i at that iteration. defer func() { fmt.Println(i) }() follows loop-variable rules (shared before 1.22; per-iteration on go 1.22+). Pass i or i := i if versions mix.