Why is `defer f.Close()` on a file you just wrote an error-swallowing bug?
answer
- a deferred call's result has nowhere to go
- closing the file is not flushing your writer
- kernel may report the write at close
- defers run last-in-first-out
- close explicitly where you can still return
basics
~20 sA deferred call's return value is thrown away, so defer f.Close() discards the error reporting a failed write. Closing the file also does not flush a bufio.Writer above it, so buffered bytes are never written and the run still reports success.
solid answer
~50 s`defer f.Close()` runs the call and drops its result on the floor — there is nowhere for a deferred call's return value to go. On a write path that is the one error you most need. Two distinct losses hide behind it. First, `Close` on `*os.File` can report an I/O failure that no earlier `Write` reported, because the kernel may only surface a failed write when the descriptor closes; discard it and a full disk looks like a clean run. Second, if you wrapped the file in a `bufio.Writer`, closing the file flushes nothing of yours — whatever is still in that writer is silently never written, and you do not even get an error. The fix is to flush and close explicitly on the success path and check both errors, keeping the deferred `Close` only as a safety net for early returns.
code
go · 15 linesfunc writeReport(path string, rows [][]string) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close() // discards the error that reports a failed write
w := bufio.NewWriter(f)
for _, r := range rows {
if _, err := w.WriteString(strings.Join(r, ",") + "\n"); err != nil {
return err
}
}
return nil // whatever is still in w is never written
}go deeper
Remember the shape of the bug: a deferred call discards whatever it returns, so defer f.Close() on a file you wrote to throws away the error. Know that closing a file does not flush a bufio.Writer sitting on top of it.
Explain both losses separately — the delayed I/O error that only Close reports, and the buffered bytes that Close never writes — and show the fix that flushes and closes explicitly while keeping a deferred Close for early returns.
Demonstrate that you treat the write path as a place where an unchecked return equals silent data loss: what you require in review, and how a job proves it wrote what it read before it exits zero.
Be ready to argue for a codebase-wide rule on close-and-flush handling, and to weigh the cost of enforcing it in tooling against the cost of one silently truncated output nobody noticed for a quarter.
## What `defer` does with the result `defer f.Close()` schedules the call to run when the surrounding function returns. The call happens; its return value has nowhere to go and is discarded. This is not a special rule about `Close` — a deferred call *can* return values, there is simply no expression context to receive them. So the line reads, to the compiler, exactly like `f.Close()` with the error thrown away — with the extra property that it is thrown away at a point in the function where you are no longer looking. On a **read** path that is usually harmless: you opened a file, you read it, closing it can fail only in ways you would not act on. On a **write** path it is one of the most damaging single lines in a Go codebase, because it converts a data-loss failure into a successful-looking run. ## Loss one: the error `Close` alone can report A successful `Write` does not mean the bytes reached durable storage. Under POSIX semantics a write can be accepted and then fail later — the classic cases are running out of space or quota, and network filesystems where the failure is only discovered when the descriptor is closed. `close(2)` is allowed to report that failure, and Go's `(*os.File).Close` returns it. So `Close` is the last, and sometimes the only, place a failed write is reported. Discarding it is discarding the report. ## Loss two: the bytes that were never written at all The more common production version has nothing to do with the kernel. A rolled-up report is usually written through a buffered writer: ```go f, _ := os.Create(path) w := bufio.NewWriter(f) cw := csv.NewWriter(w) ``` `f.Close()` closes the descriptor. It has no idea that `bufio.Writer` and `csv.Writer` are sitting on top of it holding bytes you handed them. Those bytes are simply never written, and no error is produced anywhere, because from the file's point of view nothing went wrong. The output file is short by up to one buffer's worth of rows — a partial final record is a common signature — and the process exits zero. ## The `defer` ordering trap that follows The usual attempted fix is to defer the flush too: ```go defer f.Close() w := bufio.NewWriter(f) defer w.Flush() ``` Deferred calls run last-in-first-out, so `w.Flush()` runs before `f.Close()` here, which is the order you need. Reverse the two `defer` statements and you flush into a closed file. Either way, both errors are still discarded — the ordering fix does not touch the swallowing problem at all. ## What to write instead Do the closing work explicitly, on the path where you still have somewhere to return an error to: ```go if err := w.Flush(); err != nil { return err } if err := f.Close(); err != nil { return err } ``` Keep `defer f.Close()` above that as a safety net for the early-return paths; the second close simply returns an error you deliberately ignore. The other common shape assigns the close error to a **named result** from inside a deferred closure, so the function's returned error picks it up when nothing else failed. Both are accepted; what is not accepted is a write path where the last error the program could have seen is discarded by a `defer`. ## One trap worth knowing by name `csv.Writer.Flush()` returns nothing at all. The error is retrievable only through `csv.Writer.Error()`, so a reviewer cannot even spot a missing check by looking for a discarded return value — the code simply has to remember to ask. On a CSV write path, `Flush` followed by `Error` is the check. ## How this shows up in production A nightly job reads CSV exports, rolls them up and writes one output file. It has run for months. One night the volume fills, or the last buffer never flushes, and the job exits zero with a file missing a quarter of its rows. Every downstream consumer treats the file as authoritative because the run succeeded. The postmortem finding is one line long: the error that would have failed the run was returned by a deferred call, and nothing ever read it.
- If you keep the deferred Close as a safety net and also close explicitly, what does the second Close return?An error saying the file is already closed. That one is genuinely safe to discard, because the meaningful close already happened and was checked. Write the deferred call as `defer f.Close()` for the early-return paths and let the explicit `f.Close()` on the success path carry the error you actually return.
- Why can a `Write` that returned no error still correspond to bytes that never reach the disk?Two different reasons. In user space, a `bufio.Writer` accepts the bytes into memory and only writes through on `Flush` or when full. In the kernel, a write can be accepted and then fail — out of space, quota exceeded, a network filesystem going away — and that failure may only be reported when the descriptor is closed.
- How do you check a failed write when using `csv.Writer`, whose Flush returns nothing?Call `Flush` and then `Error`. `csv.Writer.Flush()` has no return value at all, so the only way to learn that a record failed to write is `if err := cw.Error(); err != nil`. It is the one place on a CSV write path where a missing check leaves no discarded return value for a reviewer to spot.
saying these in an interview costs you the question
- Thinks a deferred call's return value goes somewhere
- Believes closing the file flushes a bufio.Writer wrapped around it
- Says Close can only fail if Write already failed
- Treats defer f.Close() as correct on write paths because it is on read paths
- Fixes the defer ordering and considers the swallowed error handled