In Go, how do you make a deferred f.Close() failure reach the caller as the function's error?
answer
- a deferred call has nowhere to return to
- the result variable is still assignable
- guard the assignment on the existing error
- one line keeps both: errors.Join
basics
~20 sGive the function a named error result and defer a closure instead of the bare call. Inside it, call Close and assign its error to that result only when the result is still nil, so a real earlier failure is never overwritten.
solid answer
~50 sA bare `defer f.Close()` has nowhere to put its error, so you declare the function's result as a named `(err error)` and defer a closure that assigns into it. Deferred calls run after the return value has been set, and a named result is still assignable at that point, so the closure can change what the caller sees. The important detail is precedence: write `if cerr := f.Close(); cerr != nil && err == nil { err = cerr }`. Assigning unconditionally — `defer func() { err = f.Close() }()` — is a real bug, because a successful close then replaces a genuine write error with nil and the failure disappears entirely. If you want both, `errors.Join(err, f.Close())` keeps whichever are non-nil and returns nil when neither is. Reserve all of this for handles you wrote to; on a read-only handle there is nothing worth propagating.
code
go · 13 linesfunc writeAll(path string, data []byte) (err error) {
f, err := os.Create(path)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr
}
}()
_, err = f.Write(data)
return err
}go deeper
Know that a bare deferred close cannot report anything, and recognise the shape of a named error result with a deferred closure assigning into it.
Be able to explain why it works — the result is assigned before the deferred calls run — and write the precedence guard correctly from memory.
Show judgment about which error wins and why, and be ready to say where the pattern does not belong so it stays a signal rather than boilerplate.
Own the convention across a codebase: one documented shape for cleanup errors on write paths, so reviewers can spot a missing one without arguing it case by case.
### Why a bare deferred Close cannot report anything `defer f.Close()` schedules a call whose result is discarded — the deferred call has no caller to return to. To capture the value you need somewhere to put it, and the only variable still alive and still connected to the caller at that moment is a **named result**. ### The mechanics When a function with a named result returns, the return value is assigned to that named variable first, and only then do the deferred calls run. Because the deferred closure captures the variable rather than a copy of it, an assignment inside the closure changes what the caller receives. ```go func writeAll(path string, data []byte) (err error) { f, err := os.Create(path) if err != nil { return err } defer func() { if cerr := f.Close(); cerr != nil && err == nil { err = cerr } }() _, err = f.Write(data) return err } ``` At the top level of the body, `f, err := os.Create(path)` reuses the named `err` rather than creating a new one, because short variable declaration only needs one new name on the left and the named result lives in the function's outermost scope. Inside an `if` or `for` block the same line would introduce a different variable, and the closure would keep reading the outer one. ### The precedence rule, and the bug people write instead The guard `cerr != nil && err == nil` encodes a policy: **the first failure is the interesting one.** A write that fails usually makes the close fail too, and the write error is the one with the useful context. Two wrong variants show up constantly: - `defer func() { err = f.Close() }()` — a successful close assigns nil over a real write error, so the function reports success on a failed run. This is strictly worse than not capturing the error at all. - `defer func() { if cerr := f.Close(); cerr != nil { err = cerr } }()` — closer, but a failing close now masks the write error that caused it, and you debug the wrong layer. ### Keeping both errors Since Go 1.20, `errors.Join` takes any number of errors, drops the nil ones, and returns nil when they are all nil. That makes a one-line deferred closure possible: ```go defer func() { err = errors.Join(err, f.Close()) }() ``` The joined value still matches both underlying errors under `errors.Is`, so callers testing for a specific condition are not broken by the extra error. The tradeoff is a message that concatenates two lines; when a function's error is going to be shown to a user rather than inspected, the explicit `err == nil` guard usually reads better. ### Multiple layers When there is a buffered writer over the file, the closure has to finish the layers innermost first, and every layer's error deserves the same treatment: ```go defer func() { if ferr := w.Flush(); ferr != nil && err == nil { err = ferr } if cerr := f.Close(); cerr != nil && err == nil { err = cerr } }() ``` A single closure that finishes the whole stack is easier to reason about than a chain of separate deferred statements, because the precedence between the layers is written down instead of implied. ### When not to bother This machinery buys nothing on a handle you only read from: there is no unwritten data at stake, and a close error there is noise you cannot act on. Adding a named result and a closure to every function that opens a file makes the ones that genuinely matter harder to spot. Use it where the function's job was to produce bytes, and leave the read paths plain. ### The review test Given a function that opens a file for writing, ask two questions: can a failure at close change the caller's decision, and would a successful close ever hide an earlier failure? The first decides whether you need the named result; the second decides the guard.
- What breaks if the deferred closure assigns the close error unconditionally?A successful close writes nil over a real error. If the write failed and the close then succeeded, the function returns nil and the caller records the operation as done. That is worse than never capturing the close error, because it turns a loud failure into a silent one. The guard `cerr != nil && err == nil` exists exactly to prevent it.
- Why does this need a named result rather than a plain return type?Deferred calls run after the return value has been assigned. With an unnamed result there is no variable left for the closure to touch, so nothing it computes can reach the caller. A named result is still in scope and still assignable at that moment, which is what lets the closure change the returned error.
- Should every function that opens a file use this pattern?No. It earns its complexity on handles you wrote to, where the close can be the only report of lost data. On a read-only handle there is nothing to propagate, so a plain deferred close keeps the code readable and keeps the pattern meaningful where it appears.
saying these in an interview costs you the question
- assigning err = f.Close() unconditionally in the deferred closure
- expecting an unnamed result to be modifiable from a defer
- letting a close error mask the write error that caused it
- adding the pattern to read-only paths where nothing is at stake
- thinking the closure gets a copy of the result variable