Test Helpers in Go and Why t Helper Matters for Failures
Without t.Helper a failing check inside a helper is reported at the helper's line. See it fail, fix it, and learn why every helper layer needs the call.

A test helper is a function that does a check on behalf of a test: compare two values, load a fixture, start a server. Helpers remove repetition and make tests read like specifications. They have one failure mode that makes the output worse than the repetition they replaced: when a check inside a helper fails, the failure message points at the line inside the helper, and that line is the same for every caller.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
t.Helper() fixes it. This tip shows the problem and the fix with real output, and the case that catches people: helpers that call helpers. It goes with your first test and the failure messages in table-driven tests.
The problem and the fix
Two helpers do the same comparison. One calls t.Helper(), the other does not.
package main
import "testing"
// Without t.Helper, a failure is reported at the line inside this function, which is the same for every caller.
func assertEqualBad(t *testing.T, got, want int) {
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
// With t.Helper, the failure is reported where the helper was called, which is the line you need to look at.
func assertEqualGood(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestWithoutHelper(t *testing.T) {
assertEqualBad(t, 1+1, 3)
assertEqualBad(t, 2*2, 5)
}
func TestWithHelper(t *testing.T) {
assertEqualGood(t, 1+1, 3)
assertEqualGood(t, 2*2, 5)
}
// Helpers that call helpers: every layer needs t.Helper, or the report points at the layer that forgot.
func mustPositive(t *testing.T, n int) {
t.Helper()
requireTrue(t, n > 0, "expected a positive number")
}
func requireTrue(t *testing.T, ok bool, msg string) {
if !ok {
t.Fatal(msg)
}
}
func TestNestedHelper(t *testing.T) {
mustPositive(t, -4)
}package main
func main() {}for name in TestWithoutHelper TestWithHelper TestNestedHelper; do
echo "\$ go test -run '^$name\$'"
go test -count=1 -run "^$name\$" . 2>&1 | grep -E '^(--- FAIL| helper_test)' | sed -E 's/ \([0-9.]+s\)//'
done$ go test -run '^TestWithoutHelper$'
--- FAIL: TestWithoutHelper
helper_test.go:8: got 2, want 3
helper_test.go:8: got 4, want 5
$ go test -run '^TestWithHelper$'
--- FAIL: TestWithHelper
helper_test.go:26: got 2, want 3
helper_test.go:27: got 4, want 5
$ go test -run '^TestNestedHelper$'
--- FAIL: TestNestedHelper
helper_test.go:38: expected a positive numberEach test calls its helper twice with a wrong answer.
Without t.Helper():
helper_test.go:8: got 2, want 3
helper_test.go:8: got 4, want 5Both failures are reported at line 8, the t.Errorf inside the helper. They look as if the same check failed twice. In a test with twenty assertions, you cannot tell which ones failed without reading the values and searching for them.
With t.Helper():
helper_test.go:26: got 2, want 3
helper_test.go:27: got 4, want 5Now each failure is attributed to the line in the test where the helper was called. Click the line in your editor and you are at the failing check.
What t.Helper does
t.Helper() marks the calling function as a helper. When the testing package builds the file and line for a log message, it walks up the call stack and skips frames that are marked as helpers, stopping at the first one that is not. So the reported location becomes the code that called the helper. It costs nothing worth worrying about, and it should be the first line of the function, before anything that might log or fail.
It applies to t.Log, t.Error, t.Fatal and their formatted variants, to t.Skip, and to *testing.B and *testing.F the same way.
The layered case
The third test is the one that bites. mustPositive calls t.Helper() and delegates the check to requireTrue, which does not. The test calls mustPositive(t, -4) on line 43. The report:
helper_test.go:38: expected a positive numberLine 38 is the t.Fatal inside requireTrue. The stack walk skipped mustPositive (a helper) and stopped at requireTrue (not marked), and reported there. The caller's line 43 never appears. The rule: every function in the chain between the test and the failing call needs t.Helper(). If a helper delegates to another function that can fail the test, that function needs it too.
Habits
- Make
t.Helper()the first line of any function that takes a*testing.Tand fails it for its caller. It is cheap and wrong to omit. - Do not mark the test function itself, or functions that are the meaningful place for the failure to be reported (a function whose own line is informative).
- Prefer helpers that take
tand fail over helpers that return an error for the test to check, when the check is always the same: the call site stays one line. - Use
t.Cleanupin helpers that create resources (a temp file, a server), so callers do not need a matching teardown line (cleanup). - Name helpers for what they assert or build:
mustParse,assertJSONEqual,newTestServer. - Consider
t.Fatalversust.Errordeliberately: a setup helper that cannot continue should callFatal; a comparison helper should usually callErrorso the test shows every difference.
The helper machinery is described in the testing package documentation for T.Helper.


