Why does Go's compiler reject an unused local variable but accept an unchecked returned error?
answer
- hygiene rules, not safety rules
- declarations versus results
- checked exceptions were rejected on purpose
- convention plus tooling, not the compiler
basics
~20 sAn error is an ordinary return value, not a contract the compiler tracks. Go rejects unused variables and imports as build hygiene, but it deliberately has no checked exceptions and no must-use rule, so discarding a returned error compiles cleanly.
solid answer
~50 sThe two rules solve different problems. Unused locals and unused imports are rejected because they are almost always leftovers that make a build slower and a file harder to read — a hygiene rule about *declarations*. An unchecked error is a rejected *safety* rule: Go's designers argued that binding error handling to a control structure and to compiler obligations, as checked exceptions do, produces convoluted code and pushes people into empty handlers written only to satisfy the compiler. So `f.Close()` on its own line, or a call whose `(int, error)` results are simply not assigned, is legal Go. `go vet` does not flag it either — its analyzers cover things like printf format mismatches, not unchecked results. The check is a convention enforced by review and by a linter in CI, and that gap is the most-criticised part of the design.
code
go · 6 linesfunc demo() {
// compile error: n declared and not used
n := 42
// legal: fmt.Println's (int, error) results are simply dropped
fmt.Println("x")
}go deeper
Know the two rules concretely: an unused local variable or import will not build, while a call whose error result you never look at will. Do not expect the compiler to protect you here.
Explain the distinction between a hygiene rule on declarations and a safety obligation on types, and be able to say why checked exceptions were rejected rather than simply that Go lacks them.
Demonstrate how you close the gap in practice: what your CI linter is configured to allow, what you flag in review, and which unchecked calls you consider genuinely acceptable.
Own the policy trade-off — how strict the linter should be before its false positives train people to add blanket suppressions, and what the team agrees is a legitimate best-effort call.
## Two rules that look inconsistent Go is famously strict about small things. Declare a local variable and never use it, or import a package you never reference, and the build fails outright — not a warning, an error. Yet this compiles without a murmur: ```go func demo() { // fmt.Println returns (int, error); discarding both is legal fmt.Println("x") } ``` The apparent inconsistency is real, and it is worth understanding because it defines exactly how much safety Go's error model gives you. ## Why unused declarations are errors The unused-variable and unused-import rules are about *declarations*, and they exist for mechanical reasons: an unused import forces the toolchain to load and compile a package for nothing, and an unused local is nearly always the residue of an edit — a line someone stopped using, or a value they meant to pass and did not. Making these hard errors keeps files honest and builds fast, and because they are trivially mechanical to fix, the cost of strictness is near zero. Note the boundaries: unused *struct fields* and unused *function parameters* are perfectly legal, because those are part of an interface to other code rather than local residue. And assigning to the blank identifier counts as a use, which is the escape hatch. ## Why the error return is not enforced Enforcing that a returned error is inspected would be a different kind of rule: a safety obligation attached to a *type*. Go's designers considered the checked-exception version of that idea and rejected it. Their argument, in short: coupling error handling to a control structure produces convoluted code, and forcing declarations and handlers on every caller pushes programmers to label ordinary failures as exceptional, or to write empty handlers purely to satisfy the compiler. A rule people routinely subvert buys less safety than it looks like it buys. Go also has no general must-use annotation. There is no way to mark a result as "the caller must consume this" — not for `error`, not for anything else. So the language ends up with a convention ("check the error") that has full social force and zero compiler force. ## What the toolchain does and does not do `go vet` ships with the toolchain and runs a set of analyzers — printf format mismatches, misused struct tags, lost context cancel functions, copied locks and so on — and `go test` runs a subset of them by default. **Unchecked error results are not among them.** That is a deliberate scoping decision: vet aims for high-confidence findings with essentially no false positives, and "this error was not checked" has too many legitimate exceptions (a `Close` on a read-only file, a deliberate best-effort write) to belong there. So in practice teams close the gap two ways: code review, where an error block without a return or a bare call that returns an error is a routine comment; and a dedicated linter in CI configured with the exclusions the team accepts. ## The cost, stated honestly This is the weakest point of Go's error model and you should be able to say so plainly. The language makes every failure path *visible* but does not make any of them *mandatory*. The result is that the most damaging error-handling bugs in Go are not mishandled errors, they are unobserved ones: a write whose result nobody read, a `Close` that silently failed to flush, a check whose body forgot to return. A reviewer who knows this reads error handling first, not last. ## What you gain in exchange The payoff is that error handling stays ordinary code. There is no separate declaration syntax to keep in sync with the implementation, no obligation that ripples up through every intermediate signature when a leaf function starts failing in a new way, and no incentive to widen a signature just to avoid touching callers. You choose the discipline and you enforce it with tooling — which is the same bargain Go makes about formatting and about most other style questions.
- Does go vet report a returned error that is never checked?No. The vet suite targets high-confidence problems such as printf format mismatches, copied locks and a context cancel function that is never called, and `go test` runs a subset of those by default. Unchecked results are out of scope because too many are deliberate. Teams that want the check add a dedicated linter to CI.
- Why are unused struct fields and unused function parameters legal when unused locals are not?Because they are part of an interface to other code, not local residue. A field may exist for serialisation or for a future caller, and a parameter may exist to satisfy a function signature or an interface method. A local variable nobody reads, by contrast, is nearly always a leftover from an edit.
- What would checked exceptions have cost Go?Signature churn and dishonest handlers. Every new failure mode in a leaf function would ripple declarations up through every intermediate caller, and the pressure to avoid that churn pushes people towards catch-and-ignore blocks or a single very broad declared type. Go's designers judged that the enforcement is routinely subverted and so buys less than it appears to.
saying these in an interview costs you the question
- Claims go vet fails a build on an unchecked error
- Says Go has checked exceptions like other languages
- Believes the compiler forces every error to be handled
- Cannot name any way the team enforces the check