Defer and recover: the compiler errors¶
Level: 201 · anyone reading a defer or recover error message for the first time
One line: The compiler holds defer to one shape — a function call, not parenthesized, not a conversion, not a built-in whose result would be lost — and holds recover to a call with no arguments; and it decides for itself that a panic statement ends a function while a call to a helper that panics does not.
Each entry shows a program that does not build, the compiler's exact words on Go 1.25, and the fix. The drivers record the messages, so a later Go that rewords them shows up as a failed check.
expression in defer must be function call¶
The reader wanted f.Close to run at exit and left off the parentheses, as one might after seeing a method value passed around.
Verified output of defer_err_not_a_call_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
$ go build not_a_call.go
# command-line-arguments
./not_a_call.go:12:8: expression in defer must be function call
./not_a_call.go:13:8: expression in defer must not be parenthesized
./not_a_call.go:15:8: defer requires function call, not conversion Cents(total) (value of int type Cents)
exit status 1
The mistake. Line 12: defer f.Close is a method value, not a call. defer needs the call, parentheses and all, so that it knows which arguments to save now (defer evaluates its arguments now).
The fix.
expression in defer must not be parenthesized¶
Line 13 of the same program: defer (f.Close()), parentheses around the call, as in a language where defer takes an expression.
The mistake. The spec ↗ requires the expression to be a function or method call and says it cannot be parenthesized — the same rule as for go.
The fix. Drop the outer parentheses: defer f.Close().
defer requires function call, not conversion¶
Line 15: defer Cents(total) looks like a call, but Cents is a type, so this is a conversion — it produces a value and calls nothing.
The mistake. There is nothing to run later. The reader meant a function that takes the converted value, or a method on the type.
The fix.
defer discards result of append¶
append returns the new slice; a deferred append would compute it and drop it, so the compiler refuses. len is the same case.
Verified output of defer_err_builtin_result_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
$ go build builtin_result.go
# command-line-arguments
./builtin_result.go:7:8: defer discards result of append(audit, "finished") (value of type []string)
./builtin_result.go:8:8: defer discards result of len(audit) (value of type int)
exit status 1
The mistake. The spec ↗ rules out the built-in functions that are not permitted in statement context — append, cap, len, make, new and the rest of the list under Expression statements ↗. panic, print, close, delete and recover are permitted, which is why defer close(ch) compiles.
The fix. Defer a closure that assigns the result:
recover (built-in) must be called¶
recover was stored in a variable to defer later, and then called with the panic value it was supposed to catch.
Verified output of defer_err_recover_misuse_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
$ go build recover_misuse.go
# command-line-arguments
./recover_misuse.go:4:13: recover (built-in) must be called
./recover_misuse.go:6:8: invalid operation: too many arguments for recover("invoice 18 has no customer") (expected 0, found 1)
exit status 1
The mistake. Built-in functions are not values: the spec ↗ says they can only be used in call expressions. And recover takes no argument — it returns whatever the goroutine is panicking with, and there is no way to ask for a particular value.
The fix.
declared and not used: r¶
The deferred recover was written and its result named, and the if around it was still to come.
Verified output of defer_err_unused_recover_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
$ go build unused_recover.go
# command-line-arguments
./unused_recover.go:7:3: declared and not used: r
exit status 1
The mistake. Go refuses an unused local variable anywhere, and a deferred function literal is no exception. recover() on its own as a statement would compile — and silently discard the value, the half-written handler that recover works only in the deferred call warns about.
The fix. Use the value — check it, log it, or turn it into an error through a named result (A defer can change a named result):
missing return¶
customerOf ends by calling fail, which always panics, and the compiler still wants a return.
Verified output of defer_err_missing_return_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
$ go build missing.go
# command-line-arguments
./missing.go:12:1: missing return
exit status 1
$ go build terminates.go
exit status 0
$ ./terminates
customer of invoice 18: ACME
exit status 0
The mistake. A function with results must end in a terminating statement ↗. A panic statement is one; a call to a function that happens to panic is an ordinary expression statement, however certain the panic is — the compiler does not look inside fail. The second program ends in panic("unknown invoice") directly, builds, and runs.
The fix. Write the panic in the function itself, as terminates.go does; or, if the helper must stay, return after it:
Sources¶
- The Go specification: Defer statements ↗, Expression statements ↗, Built-in functions ↗ and Terminating statements ↗.
- The messages come from the type checker,
go/types↗, which the compiler andgoplsshare; the wording is Go 1.25's.