skip to content

Values, Not Exceptions

A failure is an ordinary return value, so its signature, its message text, the cleanup that can produce one and the single place it finally gets handled are all API design.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

19

In Go, what does `defer f.Close()` on a file you wrote to silently discard?

level: juniorimportance: must knowfreq 58%

answer

  1. the call returns something you ignored
  2. the kernel taking bytes is not storing them
  3. a wrapper around the file has no Close
  4. os.WriteFile checks it; your code did not

basics

~20 s

Close returns an error and a deferred call discards it. On a file you wrote to, that error can be the only report that your data never reached the disk, so a failed write ends up looking like a success.

solid answer

~50 s

`Close` on an `*os.File` returns an `error`, and `defer f.Close()` evaluates the call purely for its side effect and throws the result away. That matters on the write path because a `Write` can be accepted by the kernel and still fail later — a full disk noticed at writeback, a quota, a network filesystem flushing on close — and the close is where that failure is first reported. There is a second, more common loss with the same shape: `bufio.Writer` has no `Close` method, so if you wrap the file and forget `Flush`, closing the file never pushes the last buffer out. Either way the function returns nil and the process exits 0 with a short file. The fix is to check it: the standard library's own `os.WriteFile` reports the close error whenever the write itself did not already fail.

code

go · 13 lines
go
func writeReport(path string, rows []byte) error {
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer f.Close() // the close error is dropped here

	w := bufio.NewWriter(f)
	if _, err := w.Write(rows); err != nil {
		return err
	}
	return w.Flush()
}

go deeper

for a junior

Be ready to say out loud that Close returns an error and that a deferred call throws it away. Know that on a file you wrote to, that error may be the only sign the data did not land.

for a middle

Explain the mechanics: the kernel can accept a write and fail at writeback, and a bufio.Writer holds bytes the file never sees. Be able to name Flush, Sync and Close as three different guarantees.

for a senior

Show how you would catch this in real code — checking the close error on every write path, and choosing whether Sync is part of the job's contract before you declare a run successful.

for a principal

Frame it as a data-integrity risk rather than a style nit: a silent short write costs more than a crash. Be ready to argue for a build-time check on unchecked cleanup errors in write paths.

### Close returns an error for a reason Everything closable in Go satisfies `io.Closer`, whose entire definition is `Close() error`. Writing `defer f.Close()` is legal — a deferred call may discard its results — but it means the one value the call produces is dropped on the floor. On a handle you only read from, that value is rarely actionable. On a handle you wrote to, it is frequently the *only* place a failure will ever be reported. ### Why a write can succeed and the close still fail `os.File.Write` is a thin wrapper over the operating system's `write`. The kernel accepting your bytes is not the same as the bytes being stored, and several ordinary failures are only discovered afterwards: - **Delayed allocation.** On filesystems such as ext4, blocks may not be allocated at write time. A full disk or an exceeded quota then surfaces during writeback, and the close can be the first call in a position to observe it. - **Network filesystems.** NFS clients famously buffer and flush on close; a `close()` returning a no-space or quota error is the canonical example, and Go surfaces it as the error from `f.Close()`. - **Pipes, sockets and devices exposed as files**, where the peer going away is noticed at teardown. The rule is the same one C programmers learned the hard way: check the result of closing anything you wrote to. ### The second loss: a buffer that has no Close `bufio.Writer` has `Write`, `Flush`, `Buffered` and `Reset` — it has **no** `Close` method at all. Wrapping a file with `bufio.NewWriter(f)` and deferring only `f.Close()` means the final partial buffer is never handed to the file descriptor. The program exits 0 and the output is short by up to one buffer's worth of bytes, which is why this bug is usually noticed as a row-count mismatch rather than as an error. The same trap appears with wrappers that *do* have a `Close`: a `compress/gzip.Writer` writes its footer in `Close`, and closing the underlying file does not close the gzip writer. An `encoding/csv.Writer` needs `Flush` and then a check of its `Error` method. Closing the file never reaches back up the chain — each layer must be finished in its own right, innermost first. ### Flush, Sync and Close are three different guarantees - `w.Flush()` moves bytes from a user-space buffer into the file descriptor. - `f.Sync()` asks the operating system to push them to stable storage; it is the fsync call. - `f.Close()` releases the descriptor and reports whatever the filesystem reports at that point. If your job's contract is 'the file is durably on disk before I record the run as complete', `Close` alone does not give you that; `Sync` before `Close` does, and both errors have to be checked. If the contract is only 'the data reached the OS', `Flush` plus a checked `Close` is enough. ### What to write instead The minimal correct shape is the one the standard library uses in `os.WriteFile`: keep the write error, and let the close error take its place only when there is no write error yet. ```go _, err = f.Write(data) if cerr := f.Close(); cerr != nil && err == nil { err = cerr } return err ``` When the writes happen further down the function, the same effect is achieved with a named result and a deferred closure that assigns into it. On a read-only handle, `defer f.Close()` remains fine — and writing `defer func() { _ = f.Close() }()` makes the discard deliberate rather than accidental, which is what a reviewer wants to see. ### Why this defect survives so long There is no panic, no log line, and no non-zero exit status. The only symptom is data that is quietly incomplete, discovered days later by whoever reconciles counts. That asymmetry — cheap to write correctly, expensive to detect — is the reason interviewers use `defer f.Close()` as a code-review question.

  • If Write already returned nil, what is actually left for Close to fail on?
    The kernel accepting bytes is not the same as storing them. With delayed allocation, blocks are assigned during writeback, so a full disk or an exhausted quota can surface after every `Write` returned nil. Network filesystems buffer and flush at close for the same reason. `Close` is the last point at which the filesystem gets to tell you the file is not what you think it is.
  • Does closing an os.File flush a bufio.Writer that wraps it?
    No. The wrapper holds bytes in its own buffer in your process; the file knows nothing about it. `bufio.Writer` has no `Close` method, so you must call `Flush` yourself and check its error before closing the file. Layered writers finish innermost first: flush or close the wrapper, then close the file.
  • Is Close enough to say the file is safely on disk?
    No. `Close` releases the descriptor and reports pending filesystem errors, but it does not fsync. If the requirement is surviving a machine crash, call `f.Sync()` before `Close` and check both errors. If the requirement is only that the bytes reached the operating system, a flushed writer plus a checked `Close` is sufficient.

Deferring Close on a file you wrote to is like handing over a payment and walking out without looking at the receipt — the one piece of paper that would have told you the transaction was declined.

saying these in an interview costs you the question

  • Close cannot fail once the writes returned nil
  • closing the file flushes any writer wrapping it
  • Close does an fsync, so the data is durable
  • ignoring Close is fine because the process is exiting anyway
  • a short output file must mean the input was short
open as a page

What does Go's built-in error interface require a type to implement?

level: juniorimportance: must knowfreq 85%

basics

~20 s

error is a predeclared interface with exactly one method, Error() string. Any type that declares that method - a struct, a named int, anything - is an error and can be returned wherever error is expected.

open as a page

Why is logging an error with slog.Error and also returning it to the caller a problem?

level: juniorimportance: must knowfreq 60%

basics

~20 s

Logging and returning reports one failure twice: the function writes a log record, then whoever called it reports the same failure again. Choose one role per error - either handle it and log it, or return it and stay silent.

open as a page

Why does a Go function that can fail return (result, error), and what must the caller do first?

level: juniorimportance: must knowfreq 85%

basics

~20 s

Go reports failure as an ordinary value returned last, beside the real result. The caller tests err != nil before touching the other result, because on failure that result is only a zero value, not data.

open as a page

Why does Go style require errors.New strings to start lowercase and end without punctuation?

level: juniorimportance: must knowfreq 66%

basics

~10 s

An error string is rarely printed alone. Callers wrap it, so it lands in the middle of a longer message. Lowercase text with no trailing period or newline concatenates cleanly into one readable line.

open as a page

A helper returns a nil *ValidationError as an error and the caller's check fires - what is the fix?

level: seniorimportance: must knowfreq 58%

basics

~20 s

Declare the helper's result type as error, not *ValidationError, and return a literal nil on success. A concrete nil pointer stored in an error is an interface that carries a type, so it never compares equal to nil and every successful call looks like a failure.

open as a page

In Go, how do you make a deferred f.Close() failure reach the caller as the function's error?

level: middleimportance: should knowfreq 47%

basics

~20 s

Give 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.

open as a page

When do you use errors.New versus fmt.Errorf to construct an error in Go?

level: middleimportance: should knowfreq 65%

basics

~20 s

Use errors.New when the message is fixed text. Use fmt.Errorf when the message must interpolate runtime values, since it takes a format string and arguments. Both return a value satisfying the error interface, and neither is special to the compiler.

open as a page

Why should log.Fatal never appear in a Go library or a helper function?

level: middleimportance: should knowfreq 48%

basics

~20 s

log.Fatal prints the message and then calls os.Exit(1), so no deferred function runs. Inside a library or helper it kills the caller's whole process, skips its cleanup, and removes any chance to handle the failure. Return an error instead.

open as a page

Why does `if err := step(); err != nil` leave an outer `err` variable nil in Go?

level: middleimportance: should knowfreq 58%

basics

~10 s

The short variable declaration := creates a brand-new err inside the if statement's own scope. Nothing written there reaches the outer variable, so a later return err reports nil even though a call failed.

open as a page

What is wrong with the Go error string "failed to sync: failed to fetch: connection refused"?

level: middleimportance: should knowfreq 52%

basics

~10 s

Every layer prepends "failed to", which adds nothing: the value is an error, so failure is already asserted. The repetition displaces the identifiers a reader needs and stops the chain reading as one sentence.

open as a page

A nightly Go ETL job exits 0 but some output files are short — how do you find the dropped cleanup error?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Suspect a cleanup error the job threw away. Audit every write path for an unchecked Flush, Sync or Close, reproduce with a disk quota so the failure happens on demand, then make the job check those calls and publish output only after a successful close.

open as a page

In a Go job runner, one failure produces five slog.Error lines with five different wordings. How do you fix it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Delete the log call from every layer that returns the error, and wrap instead with the operation that layer attempted. One site - where the error stops propagating - logs the whole chain once and exits non-zero.

open as a page

Why must a caller process the n bytes from io.Reader.Read before checking its error?

level: seniorimportance: should knowfreq 40%

basics

~20 s

io.Reader.Read may return n greater than zero together with a non-nil error such as io.EOF. Those bytes are real data: handle buf[:n] first, then act on the error, or the stream's tail is silently lost.

open as a page

How do you stop refactors from silently rewording a Go CLI's user-facing error text?

level: seniorimportance: should knowfreq 34%

basics

~10 s

Treat rendered error text as program output and pin it. A golden-file test compares err.Error() against a recorded string, with an -update flag to re-record, so any reworded message arrives as a reviewable diff.

open as a page

When is ignoring the error from a deferred f.Close() on a file you only read acceptable?

level: middleimportance: nice to knowfreq 33%

basics

~20 s

On a handle opened only for reading it is fine: no unwritten data is at stake, so the close error tells you nothing you can act on. The close itself is still mandatory, because skipping it holds a descriptor open.

open as a page

How do you keep a Go error message on one line when it embeds a filename supplied by a user?

level: middleimportance: nice to knowfreq 29%

basics

~20 s

Format the value with the %q verb rather than %s. It renders as a quoted Go string literal, escaping newlines, tabs and invalid UTF-8, so the message stays one line and an empty value stays visible.

open as a page

When is discarding a Go error with `_ =` defensible, and how do you mark it as deliberate?

level: seniorimportance: nice to knowfreq 32%

basics

~20 s

Only when the call cannot meaningfully fail, as with a write to a bytes.Buffer, or the failure is genuinely not actionable. Write an explicit blank assignment plus a one-line comment giving the reason, so review sees a decision.

open as a page

Where in a Go service do you allow slog.Error, and where do you require returning the error instead?

level: principalimportance: nice to knowfreq 26%

basics

~20 s

Draw the line at ownership: a package that returns an error must not report it; only layers where errors stop travelling may log. Write the exceptions down and enforce the boundary structurally, not by review habit.

open as a page