Why is defer f.Close() potentially a bug when writing files? How do you handle the Close error?
For files opened for writing, Close (or the final flush) can be where the error shows up: buffered data, NFS, a full disk. defer f.Close() throws that error away, so you can report success while the data was never written. For read-only files, ignoring the Close error is usually fine.
func WriteFile(path string, data []byte) (err error) {
f, err := os.Create(path)
if err != nil {
return err
}
defer func() {
// Keep the first error, and still report the Close failure.
err = errors.Join(err, f.Close())
}()
if _, err = f.Write(data); err != nil {
return fmt.Errorf("write %s: %w", path, err)
}
return f.Sync() // flush to stable storage if durability matters
}
The deferred closure can assign to err because it is a named result. errors.Join returns nil when both errors are nil. Note that it returns a new wrapper whenever anything is non-nil, even for errors.Join(err, nil), so callers must use errors.Is rather than ==. A common alternative is if cerr := f.Close(); cerr != nil && err == nil { err = cerr }. For bufio.Writer, call Flush() and check its error before closing. For real atomicity, write to a temp file, then call Sync, Close and os.Rename.
More on Error Handling & panics
- Q315Go errors don't carry stack traces. How do you get stack information when you need it?
- Q316"Handle an error only once." What does that mean, and what is wrong with log-and-return?
- Q318What does this print? (Shadowed
err) - Q319What does
fmt.Errorf("op: %w", err)return whenerris nil? Why does this matter in helper functions? - Q320Explain context errors:
context.Canceled,DeadlineExceeded, andcontext.Cause. - Q321Why is
errors.Is(err, fs.ErrNotExist)preferred overos.IsNotExist(err)?