skip to content

Early Returns and Nesting

Because every failure is a returned value, Go code that nests instead of returning grows an else for each error. Keeping the success path at the left margin is the habit reviewers check for.

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

questions

4

In Go, why is the success path never put inside the `else` of an `if err != nil` check?

level: juniorimportance: must knowfreq 68%

answer

  1. read the left column top to bottom
  2. the failure branch is the short one
  3. one check per call, and they stack
  4. no else after a return

basics

~20 s

Idiomatic Go handles the failure inside the if and returns, leaving the success path unindented below it. Because Go checks an error after nearly every call, putting each success in an else pushes the real work rightward.

solid answer

~50 s

Go has no exceptions, so a function of any size carries an `if err != nil` after nearly every call. The convention is to make that block the short one: handle or return the failure inside it, then continue at the same indentation as before. Written the other way, with the work inside `else`, each check costs a level of nesting, and a handler with four calls buries its one interesting line four tabs deep. Keeping the success path at the left margin is what Go reviewers mean by line of sight: you read the left column top to bottom and see what the function does, while the failure branches sit to the right and are skimmed. It also keeps each branch to one or two statements, which is what makes them individually reachable in tests.

code

go · 13 lines
go
func (s *server) handleGetUser(w http.ResponseWriter, r *http.Request) {
	id, err := strconv.Atoi(r.URL.Query().Get("id"))
	if err == nil {
		u, err := s.loadUser(r.Context(), id)
		if err == nil {
			writeJSON(w, u)
		} else {
			http.Error(w, "not found", http.StatusNotFound)
		}
	} else {
		http.Error(w, "bad id", http.StatusBadRequest)
	}
}

go deeper

for a junior

Be ready to write both shapes side by side and say which one Go reviewers expect: the failure handled and returned inside the if, the success continuing below it at the same indentation.

for a middle

Explain why the cost compounds - one check per call, each else a new block - and show the mechanical fix: invert the condition, return early, then lift the middle of the function into a helper.

for a senior

Show what the shape buys operationally. Shallow branches are individually reachable in tests, legible in a per-statement coverage report, and give a reviewer a chance of spotting a path nobody has exercised.

for a principal

Own it as a review standard rather than a personal preference: say what you would flag routinely, what you would not flag (arbitrary function-length limits), and why one shape the whole team reads identically is worth more than any single rewrite.

## The shape being described Go reports failure by returning it. There is no `try`/`catch` and no stack unwinding on an ordinary error, so a function that makes four calls that can fail contains four checks. That volume is the whole reason this convention exists: a rule about indentation that costs nothing in a language with one error site per function becomes structural in a language with one per call. The two shapes are: ``` // success inside else - the work drifts right if err == nil { ... do the work ... } else { return err } // early return - the work stays at the left margin if err != nil { return err } ... do the work ... ``` With one check the difference looks like taste. With four, the first shape ends with the interesting statement indented four levels and its matching closing braces spread over the bottom of the function, while the second keeps every statement of the normal path in a single column. ## Line of sight The phrase Go reviewers use is *line of sight*: a reader should be able to run their eye down the leftmost column of a function and read the story of what it does, with the exceptional cases parked to the right where they can be skipped on a first pass. That works only if the normal path never moves right. Once one call's success is nested, everything after it inherits that indentation, and the reader has to hold in their head which condition they are currently inside. The corollary is a small rule with a big effect: **no `else` after a branch that returns**. If the `if` body ends in `return`, `break`, `continue` or a call that cannot come back, the `else` is redundant - the code after the `if` is already the else. Deleting it un-indents everything below. ## What this is not about Two things are commonly confused with it. First, **gofmt does not do this for you**. gofmt is a formatter: it fixes whitespace, alignment and brace placement, and it will happily format a five-level nest into beautifully indented five-level nesting. Restructuring is a human edit. Second, **an `else if` chain does not add indentation**. In gofmt'd Go, `} else if cond {` sits on the closing brace line, so every condition in a chain of ten is at the same column as the first `if`. The cost this convention is about comes from nesting a block inside a block, not from chaining. That matters, because "replace the chain with a switch" is often proposed as a fix for depth and does not reduce depth at all. ## Guard clauses The same shape at the top of a function is usually called a guard clause: validate the inputs first, one condition per `if`, each returning immediately. ``` if id == "" { return nil, errMissingID } if limit < 0 { return nil, errBadLimit } // from here down, everything is known good ``` By the time the body starts, the preconditions hold, so the body needs no defensive nesting. Each guard is one reachable line with one input that triggers it. ## When the failure branch is long Sometimes handling the error genuinely takes several lines - write a response, increment a counter, clean something up. The convention still holds, because the fix is to move those lines into a small function and call it: ``` if err != nil { writeErr(w, err) return } ``` The rule is about where the *success* path lives, not about a maximum length for the failure block. Keeping the failure branch to a call plus a `return` is simply how you keep the left column intact. ## Why it is more than style A flat function has a testing property a nested one does not. Each branch is reached by one condition, so one input exercises it, and a per-statement coverage report shows plainly which branches nothing has run. In a five-level nest, reaching the innermost branch requires satisfying four outer conditions, so tests carry setup unrelated to what they check - and branches that are hard to reach in a test are usually branches nobody has read either. That is the practical argument to make in review when someone treats the shape as a preference: the nesting is what let an unread branch survive into production. ## What a reviewer says The useful review comment is mechanical rather than aesthetic: "invert this condition and return; the rest of the function moves left one level." It is a safe, local edit, it is obvious in the diff, and it is the same edit every time.

  • Does an `else if` chain in gofmt'd Go actually indent one level further with each condition?
    No. gofmt keeps `} else if cond {` on the closing brace line, so every condition in the chain sits at the same column as the first `if`. The cost this convention is about comes from putting a block inside a block, or putting the work inside `else`, not from chaining conditions. That is why the fix for a deep function is early returns and extraction rather than rewriting the chain.
  • What if handling the failure genuinely takes ten lines - does the shape still hold?
    Yes. Move the ten lines into a small function and call it from inside the `if`, then return. The convention is about where the success path lives, not about a length limit on the failure block. Keeping that branch to a call plus a `return` is what preserves the left column, and it usually improves the failure handling too by giving it a name.
  • What does the guard-clause shape at the top of a function look like?
    One condition per `if`, each returning immediately: empty id, negative limit, nil dependency. By the time the body starts, everything is known good, so the body needs no defensive nesting. Each guard is a single reachable statement with a single input that triggers it, which makes both the test and the coverage report trivial to read.

It reads like a checklist with exits: each line either sends you out the door or lets you continue down the page. Nesting turns the same checklist into a set of nested boxes you must open in order.

saying these in an interview costs you the question

  • Claims the Go compiler rejects an else after a return
  • Thinks gofmt restructures nesting rather than only formatting it
  • Says an else-if chain adds one indentation level per condition
  • Puts the entire function body inside if err == nil
  • Treats nesting as pure taste with no testing consequence
open as a page

In Go, why is `defer` placed on the line right after the call that acquires a resource?

level: middleimportance: should knowfreq 56%

basics

~20 s

Putting the release right below the acquisition lets a reader check the pair at a glance, and it covers every exit added below. It belongs after the error check, so it never releases what was never acquired.

open as a page

A Go HTTP handler nests error checks five levels deep and panicked on its deepest branch. How would you restructure it in review?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Invert each check into a guard that returns, so every branch sits at one level, then lift the middle into a helper returning a value and an error. The deep branch panicked because no reader and no test reached it.

open as a page

When would you replace a long if/else-if chain in Go with a tagless `switch`?

level: middleimportance: nice to knowfreq 40%

basics

~20 s

A Go switch with no expression after the keyword compares each case against true, so every case is an ordinary condition. Use it when a function classifies: the conditions align in one column and default names the catch-all.

open as a page