A long exported Go function with named results ends a branch with a bare return, and callers get a zero value and a nil error. How do you find and prevent this?
answer
- the results already exist before you return
- an empty return is not an empty answer
- one branch never touched the second result
- assert the value on the failing input too
basics
~20 sA bare return sends back whatever the named results currently hold, so a branch that forgets to set the error result returns a zero value with a nil error. Catch it with a test that asserts the returned value on the failing branch, not just the error.
solid answer
~50 sNamed results are declared with the signature and start at their zero values, and a bare `return` hands back whatever they hold at that moment. In a short function you can see that; in a 100-line one you cannot, and the branch that returns early without assigning the error result gives every caller a zero value with a nil error — indistinguishable from a real answer. The usual cause is a missed assignment or an `err` shadowed by `:=` inside an `if` block, and the standard `go vet` suite does not flag either. I find it with a table test that asserts both results for a failing input, since a test checking only `err != nil` passes here for the wrong reason. Prevention is a style rule: explicit returns that name every value, and functions short enough that the results are visible.
code
go · 17 linestype Decision struct {
On bool
Reason string
}
var ruleSets = map[string][]bool{}
func Evaluate(env string) (d Decision, err error) {
rules, ok := ruleSets[env]
if !ok {
// Nothing assigns err here. The bare return hands back the
// zero Decision with a nil error, which callers read as "off".
return
}
d.On = len(rules) > 0
return d, nil
}go deeper
Know that named results are variables starting at their zero values, and that a bare return sends those current values back. Prefer writing every value out in the return statement.
Explain how a missed assignment or a shadowed err leaves the named error nil, and why the resulting zero value with a nil error is indistinguishable from a real answer at the call site.
Show the diagnosis: a table test asserting both results for failing inputs, since a test that only checks the error can pass for the wrong reason. Be ready to say why the default vet checks do not catch it and how you would sweep a codebase.
Turn it into a written rule with a cheap exception — explicit returns everywhere, named results for documentation and deferred error decoration — and defend the function-length limit that keeps the class from reappearing.
## What a bare return actually does A Go function may name its results: ``` func Evaluate(env string) (d Decision, err error) ``` Those names are ordinary variables, declared at function entry and initialised to their zero values — `Decision{}` and `nil` here. A bare `return`, with no expressions after it, returns whatever those variables hold at that instant. It is not a shortcut for "return nothing"; it is "return the current contents of the result variables". That is exactly why it is dangerous at a package boundary. ## The failure Consider an evaluation function in a feature-flag package, keyed by flag and environment, that grew to a hundred lines as rule kinds were added. One branch handles "no rule set for this environment" and ends with a bare `return`. The author meant "there is nothing here"; what the caller receives is a zero `Decision` and a nil `err`. From the caller's side that is a *successful* evaluation reporting that the flag is off. Nothing logs, nothing retries, nothing alerts. The feature is silently disabled for one environment, and the package's own tests pass because they assert on the branches that do set the error. Two mechanisms produce this, and both hide well in a long body: - **A missed assignment.** The branch was added later and simply never sets `err`. - **Shadowing.** Inside a block, `if v, err := load(env); err != nil { ... }` declares a *new* `err` scoped to the `if`. Assigning to it does not touch the named result, so a later bare `return` still hands back nil. The standard `go vet` checks run by `go test` do not report shadowed variables; the shadow analyser lives outside the default suite. ## Finding it The diagnostic is a unit test that asserts the *value*, not only the error. A test written as ``` _, err := Evaluate("staging") if err == nil { t.Fatal("want error") } ``` fails correctly here — but the far more common test asserts only that the happy path works, or asserts a returned value and ignores the error, and both pass. So the rule for a fallible exported function is: for every failing input in the table, assert both results — a non-nil error *and* the zero or documented value — and for every succeeding input, assert a nil error and the real value. That pairing is what makes "zero value with nil error" visibly wrong instead of merely unusual. On an existing codebase, a grep for bare `return` statements inside functions with named results is a cheap first pass, and reviewing the longest such functions first finds the ones where nobody can hold the state in their head. ## Preventing it - **Write explicit returns.** `return Decision{}, fmt.Errorf("no rules for env %q", env)` states both values at the point of return, and a reviewer reading only that line knows what the caller gets. This costs nothing and removes the entire class. - **Keep result names for documentation, not for control flow.** Naming results is genuinely useful when two results share a type — `(width, height int)` — or when the names carry meaning into the generated documentation. Naming them is not a licence to stop writing values at returns. - **Keep the legitimate exception in mind.** The one case that needs named results is a deferred closure that modifies the returned error before the caller sees it — to add context or to convert a recovered panic. That is a real pattern, and it still works with explicit returns everywhere; only the *deferred* function needs the name. - **Bound function length.** The bug is a function-length bug as much as a return-style one. Nobody misplaces a result assignment in fifteen lines. - **Write the rule down.** In a style guide, one line does it: results may be named for documentation; every return statement names its values; deferred error decoration is the only exception. ## Why this belongs to signature design The caller of an exported function sees only the signature and the doc comment. If the signature says `(Decision, error)`, callers are entitled to assume that a nil error means the `Decision` is real. A bare return in a long body quietly breaks that promise for one branch, and the people who pay are in another repository.
- Why does assigning to err inside an if block sometimes leave the named err result nil?Because `if v, err := load(); err != nil` uses `:=`, which declares a new `err` scoped to that statement rather than assigning the named result. Every assignment in the block hits the inner variable, and a later bare return still sends back the outer nil. The shadow analyser is not part of the default vet checks that `go test` runs.
- Is there any case where named results are the right call?Yes: when a deferred closure has to modify what the caller receives — adding context to the error, or turning a recovered panic into an error — the result must be named for the deferred function to reach it. Two results of the same type also read better named. Neither reason requires bare returns.
- How would you sweep an existing codebase for this?Look for functions that both name their results and contain a bare return, longest first, since length is what hides the bug. For each, check every early return against the branch's intent, then add table tests that assert both results per input so the class cannot come back silently.
A bare return is like handing in a form you never filled in: it is a complete, well-formed submission, and every box says the default. The reader has no way to tell it apart from a form someone actually completed.
saying these in an interview costs you the question
- Thinks a bare return returns nothing at all
- Assumes go vet reports the missing error assignment
- Tests only that a non-nil error is returned
- Says shadowed err inside an if updates the named result
- Treats it as cosmetic style, not a caller-visible bug