Don't defer Close() on writable files (2017)

Core issue: defer Close on writable files

  • defer f.Close() in Go discards the Close error, so write failures that only surface at close time are silently ignored.
  • Several commenters stress that finalizers (defer, destructors, Drop) are for cleanup, not for operations that can meaningfully fail.
  • A common pattern suggested: defer a best-effort Close purely to avoid leaks, but also call Close (and Sync) explicitly at the point where correctness depends on success.

Go patterns and tooling

  • Idioms discussed:
    • Explicit sequence for writes: WriteSyncClose, all checked.
    • Defer a helper like CheckClose / Capture that only records a Close error if no earlier error occurred.
    • Use multi-error aggregators (errors.Join, third‑party multierr) to surface both the main error and a close error.
  • Some warn that deferring error-producing functions can hide ordering bugs: side effects may happen after a failed close, leaving inconsistent state.
  • Double-closing:
    • OS close(2) can be dangerous to retry (FD reuse race).
    • Go’s os.File.Close is explicitly idempotent and masks the syscall after first use, so “defer then explicit close” is safe at the Go level.

Exceptions vs explicit errors

  • Debate is intense:
    • Pro-exceptions: simpler “happy path”; errors propagate automatically; constructs like try-with-resources / using / context managers can attach suppressed close errors; easier to catch at appropriate abstraction level.
    • Pro-explicit errors: forces thinking about failures at call sites; improves locality and maintainability; exceptions make control flow and failure modes harder to see; large ecosystems show widespread misuse of generic catch‑all handlers.
  • Some argue modern “checked-like” sum types (Rust Result, Swift) give exception-like ergonomics with explicitness and better performance.
  • Others claim Go’s (T, error) is just “errno with sugar” and not as strong as real sum types; defenders reply that multiple return values and interface-typed errors are significantly better than a global errno.

Other languages and RAII/finalizers

  • Rust: files auto-close in Drop, but close errors are ignored; sync_all must be called explicitly for durability. There were proposals for ownership-consuming close(self) but it’s complex and unresolved.
  • C++/Java: destructors / finally can’t safely throw when another exception is in flight; languages handle double-faults poorly (terminate, swallow original, etc.). Try-with-resources / suppressed exceptions improve but don’t eliminate the issue.
  • C#, Python: using / with manage disposal; errors in Dispose/__exit__ generally surface as exceptions, sometimes replacing the original error but may be accessible via “suppressed” metadata.
  • Zig: close intentionally ignores most errors (except EBADF), reinforcing the view that resource release should be infallible; explicit flush/sync is where correctness lives.

Filesystem semantics, fsync, and durability

  • Discussion broadens to the difficulty of writing correct, durable file code on POSIX:
    • close() success does not guarantee data is on disk; writeback is buffered.
    • For new files, you often must fsync the file and its parent directory to ensure the directory entry is durable.
    • fsync failure is tricky: buffers may be invalid; retries may wrongly appear to succeed.
    • Ext4 “zero-length file” and fsyncgate are cited as examples where naive “write+rename” patterns broke under legal-but-surprising reordering.
  • Some argue most applications shouldn’t call fsync at all (performance, battery life; complexity better handled at lower layers).
  • Others counter that fsync is the only portable way to enforce ordering/consistency across crashes, and that many apps get this wrong; serious workloads should consider using SQLite or a DB rather than ad‑hoc file formats.

High-level takeaways and disagreement

  • Broad agreement:
    • Don’t ignore write/close errors you actually care about.
    • defer/RAII are great for leak prevention, weak for correctness‑critical failure reporting.
    • File and filesystem APIs are full of subtle, system-dependent edge cases.
  • Disagreement:
    • Whether Go’s explicit error style is a net win over exceptions.
    • How often Close/fsync errors matter in real-world apps versus being “theoretical”.
    • Whether problems are primarily language-level (error handling model) or OS/filesystem-level (APIs and semantics).