skip to content

Reporting Test Failures

You report failures yourself: t.Error keeps going, t.Fatal stops this test only. Called from a goroutine you spawned, t.Fatal ends that goroutine and the test can still pass.

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

questions

4

In a Go test, what is the difference between t.Error and t.Fatal?

level: juniorimportance: must knowfreq 82%

answer

  1. one keeps going, one stops
  2. Log plus Fail versus Log plus FailNow
  3. stopping happens via runtime.Goexit
  4. use the stopping one when the next line would panic

basics

~10 s

Both mark the test as failed. t.Error logs the message and lets the test function keep running, so one run can report several problems. t.Fatal logs and stops that test function immediately.

solid answer

~50 s

`t.Error` is `t.Log` plus `t.Fail`: it records the message, marks the test failed, and execution continues on the next line. `t.Fatal` is `t.Log` plus `t.FailNow`: it marks the test failed and stops the test function right there. `FailNow` stops it by calling `runtime.Goexit` on the test's goroutine, so deferred calls still run but nothing after the call does. The rule of thumb is to use `t.Fatal` when continuing is impossible or unsafe — a constructor returned an error, so the pointer you were about to dereference is nil — and `t.Error` for independent checks, so a single run tells you all three field mismatches instead of only the first. Engineers arriving from a language where assertions throw expect every failed check to abort the method; in Go only the `Fatal`/`FailNow` family does. Either way `go test` exits non-zero.

code

go · 12 lines
go
func TestParseRule(t *testing.T) {
	r, err := ParseRule("checkout_v2: on")
	if err != nil {
		t.Fatalf("ParseRule returned error: %v", err) // r is nil, so stop here
	}
	if r.Name != "checkout_v2" {
		t.Errorf("Name = %q, want %q", r.Name, "checkout_v2")
	}
	if !r.Enabled {
		t.Errorf("Enabled = false, want true")
	}
}

go deeper

for a junior

Be ready to say, without hesitation, that both mark the test failed and only t.Fatal stops the function. Know the formatted variants t.Errorf and t.Fatalf, and that t.Log alone fails nothing.

for a middle

Explain the mechanics: Error is Log plus Fail, Fatal is Log plus FailNow, and FailNow stops the test with runtime.Goexit so deferred calls still run. Be able to justify a choice line by line in a real test.

for a senior

Show the judgment behind the choice — Fatal guards a precondition whose failure would make later lines panic, Error reports independent defects so one run surfaces all of them. Point out where an over-used Fatal costs the team debugging round-trips.

for a principal

Frame it as a suite-wide readability property: tests that stop on the first guard and report everything else in one pass keep triage cheap. Be ready to argue for a house convention on message shape and guard placement rather than reviewing it case by case.

## Where failure reporting lives Go has no assertion library in the standard library. A test is an ordinary function, `func TestXxx(t *testing.T)`, in a file whose name ends in `_test.go`, and it reports problems by calling methods on the `*testing.T` value it was handed. You write the comparison yourself with a plain `if`, and then call one of the reporting methods. That is the whole model, and the only real decision it forces on you is *which* reporting method. ## The two primitives underneath Everything reduces to two methods on `testing.T`: - **`t.Fail()`** — marks the test as failed and returns. Execution continues normally. - **`t.FailNow()`** — marks the test as failed and stops executing the test function immediately. Everything else is a convenience wrapper that also logs: - **`t.Error(args...)`** = `t.Log(args...)` + `t.Fail()` - **`t.Errorf(format, args...)`** = `t.Logf(...)` + `t.Fail()` - **`t.Fatal(args...)`** = `t.Log(args...)` + `t.FailNow()` - **`t.Fatalf(format, args...)`** = `t.Logf(...)` + `t.FailNow()` `t.Log` and `t.Logf` on their own record a message and do **not** fail anything — a test that only logs is a passing test. Logged output is buffered per test and shown when the test fails (or when you ask for verbose output), which is why messages appear grouped under the test that produced them rather than interleaved. ## What "stops immediately" actually means `FailNow` does not panic and does not exit the process. It stops execution by calling `runtime.Goexit()`, which terminates the goroutine running the test function after running that goroutine's deferred calls. Three consequences follow, and they are the ones interviewers probe: 1. **Deferred calls still run.** A `defer f.Close()` earlier in the test executes. 2. **Only this test ends.** The test binary carries on with the next test function; the package result is a failure, and `go test` exits non-zero. 3. **It only ends the goroutine that calls it.** `Fatal`, `Fatalf`, `FailNow`, `Skip` and `SkipNow` must be called from the goroutine running the test function. Called from a goroutine the test started, they end that goroutine and leave the test function running. ## Choosing between them Use **`t.Fatal`/`t.Fatalf`** when the rest of the test cannot meaningfully run: - a constructor or parser returned a non-nil error alongside a nil result pointer — the next line would dereference nil and panic; - a fixture failed to load, so every later assertion would be noise; - a required precondition of the scenario is not satisfied. Use **`t.Error`/`t.Errorf`** for checks that are independent of each other. If a decoded struct has three fields and two are wrong, `t.Errorf` on each gives you both mismatches in one run; `t.Fatalf` gives you one, you fix it, rerun, and discover the second. That round-trip is the practical cost of over-using `Fatal`. A useful shorthand: *`Fatal` guards, `Error` reports.* The guard protects the code below it; the report describes a defect you already have all the information about. ## Why a nil dereference is worse than a failure If you use `t.Errorf` where `t.Fatalf` belonged, the test keeps going and dereferences the nil result. That panics. The testing framework attributes the panic to the test and the run stops there, so the remaining tests in the package never execute and you get a stack trace instead of a one-line `got/want`. Choosing `Fatal` for the error check is what keeps a failing test a *readable* failure. ## Message convention The community convention is `t.Errorf("Enabled(%q) = %v, want %v", flag, got, want)` — name the call, then got, then want. It reads as a sentence in the failure output, and it means the person triaging the run never has to open the test to know what was compared. ## The habit to unlearn when arriving from other ecosystems In frameworks where an assertion throws, every failed check aborts the method, and "assert and continue" is the unusual case. Go inverts the default: `t.Errorf` continues, and stopping is the thing you opt into. Code ported mechanically from such a framework often produces tests that keep running past a failed precondition and then panic — the single most common porting bug in Go test suites.

  • A parser returns a nil result pointer next to a non-nil error. Why is t.Fatalf the right call there rather than t.Errorf?
    Because with `t.Errorf` the test keeps running and the next line dereferences the nil pointer. That panics, the panic is attributed to the test, the run stops, and the remaining tests in the package never execute. You end up reading a stack trace instead of a one-line failure. `t.Fatalf` ends that test cleanly and lets the rest of the package run.
  • Does t.Fatal stop the current test function or the whole test binary?
    Only the current test function. `FailNow` calls `runtime.Goexit`, which ends the goroutine running that test after executing its deferred calls. The binary continues with the next test function. The package as a whole is reported as failed and `go test` exits non-zero, but no other test is skipped because of it.
  • What makes go test treat a function as a test at all?
    It has to live in a file whose name ends in `_test.go`, be named `TestXxx` where the character after `Test` is not a lowercase letter, and take exactly one parameter of type `*testing.T` with no results. A function that fails any of those is compiled but never run as a test, which is why a silent typo like `testParseRule` looks like a passing package.

t.Error is a snag written on a punch list — the inspection continues. t.Fatal is finding the stairs missing: there is nothing above worth inspecting, so the walkthrough ends there.

saying these in an interview costs you the question

  • Thinks t.Error aborts the test the way a throwing assertion does
  • Says t.Fatal abandons the rest of the package's tests
  • Uses t.Fatal for every check, so only the first mismatch is ever reported
  • Believes t.Log on its own marks a test as failed
  • Claims t.Fatal panics and could be caught by a recover
open as a page

Why does a Go test report the failure line inside a helper function, and what does t.Helper fix?

level: middleimportance: should knowfreq 52%

basics

~20 s

t.Error and t.Fatal report the file and line where they were called, which is inside the helper. Calling t.Helper() at the top of the helper makes the testing package skip that frame and report the caller's line in the test instead.

open as a page

A Go test calls t.Fatal from a goroutine it started. Why is that wrong, and what should it do instead?

level: seniorimportance: should knowfreq 45%

basics

~20 s

t.Fatal stops only the goroutine that calls it, because FailNow uses runtime.Goexit. The test function itself keeps running, or blocks forever waiting for a value that goroutine will now never send. Hand the failure back to the test goroutine instead.

open as a page

How does t.Skip in a Go test differ from just returning early from the test function?

level: middleimportance: nice to knowfreq 40%

basics

~20 s

Returning early leaves the test reported as a normal pass. t.Skip logs a reason, marks the test as skipped, and stops it immediately, so the run records visibly that the test did not actually check anything.

open as a page