In a Go test, what is the difference between t.Error and t.Fatal?
answer
- one keeps going, one stops
- Log plus Fail versus Log plus FailNow
- stopping happens via runtime.Goexit
- use the stopping one when the next line would panic
basics
~10 sBoth 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 linesfunc 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
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.
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.
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.
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