The stack trace points to the wrong line
You run your test suite. One test fails. The error message says the problem is on line 42 of test_helpers.go. You open that file. Line 42 is just a call to t.Errorf. You know the real problem is in your actual test function, but the output is pointing you at the utility wrapper. You spend time tracing the stack trace backward to find the test case that triggered the failure. This happens when you extract test logic into helper functions. The testing package gives you a tool to fix the attribution.
Marking helpers for the test runner
t.Helper() is a method on *testing.T. When you call it, you mark the current function as a helper. The test runner records this flag. When a failure occurs, the runner walks up the call stack and skips any frames marked as helpers. The error report shows the line where the helper was called, not the line inside the helper where the assertion happened. It changes the source of the failure in the output.
The testing package tracks helpers using a set of program counters. t.Helper() captures the current program counter and adds it to the set associated with that *testing.T instance. When t.Errorf or t.Fatalf runs, it inspects the call stack and filters out any frames whose program counter matches a helper. The remaining top frame becomes the source of the error. This mechanism is explicit. The compiler does not detect helpers automatically. You must opt in by calling the method.
Minimal example
Here's a helper function that checks a value. It calls t.Errorf when the check fails. Without t.Helper(), the error points to the helper.
func checkEqual(t *testing.T, got, want int) {
// Missing t.Helper() means errors point to this function
if got != want {
// Error output will show this line number
t.Errorf("got %d, want %d", got, want)
}
}
func TestCalculation(t *testing.T) {
// If checkEqual fails, the error points to checkEqual, not here
checkEqual(t, 5, 10)
}
Add t.Helper() at the top of the function. The test output changes immediately. The error now points to the line where checkEqual is called.
func checkEqual(t *testing.T, got, want int) {
// Mark this function as a helper so errors point to the caller
t.Helper()
if got != want {
// Error output now shows the line where checkEqual was called
t.Errorf("got %d, want %d", got, want)
}
}
func TestCalculation(t *testing.T) {
// Errors inside checkEqual will point to this line
checkEqual(t, 5, 10)
}
Call t.Helper() before any assertions. If you call t.Errorf before t.Helper(), the helper flag isn't set yet, and the error points to the helper. The convention is to place t.Helper() as the first statement in the function.
Helpers hide implementation. t.Helper() hides the helper.
Realistic example: table-driven tests
Table-driven tests often extract the case logic into a helper to keep the test function clean. The helper needs t.Helper() so failures point to the specific table entry. This makes it easier to identify which case failed when the test suite runs.
type testCase struct {
name string
input int
want int
fn func(int) int
}
func runCase(t *testing.T, tc testCase) {
// Mark helper so failures point to the table entry line
t.Helper()
got := tc.fn(tc.input)
if got != tc.want {
// Error points to the runCase call in the loop
t.Errorf("%s: fn(%v) = %v, want %v", tc.name, tc.input, got, tc.want)
}
}
func TestProcess(t *testing.T) {
cases := []testCase{
{name: "double", input: 1, want: 2, fn: double},
{name: "triple", input: 3, want: 6, fn: triple},
}
for _, tc := range cases {
// Errors inside runCase will point to this line
// The error message includes tc.name to identify the case
runCase(t, tc)
}
}
The error output includes the line number of the runCase call. If you have many cases, the error message should include the case name or index so you can identify the failure without looking at the line number. The helper makes the test readable. t.Helper() makes the failure readable.
Chaining helpers
Helpers often call other helpers. Every function in the chain that should be invisible in the error output needs its own t.Helper() call. If an inner helper misses the call, the error stops at that frame. The stack trace filtering works frame by frame. Each frame must be marked to be skipped.
func assertJSON(t *testing.T, got, want string) {
// Outer helper needs t.Helper() to be skipped
t.Helper()
var j1, j2 interface{}
err1 := json.Unmarshal([]byte(got), &j1)
if err != nil {
// Error points to assertJSON caller if assertJSON has t.Helper()
t.Fatalf("unmarshal got: %v", err)
}
err2 := json.Unmarshal([]byte(want), &j2)
if err != nil {
t.Fatalf("unmarshal want: %v", err)
}
// Inner helper also needs t.Helper()
assertEqual(t, j1, j2)
}
func assertEqual(t *testing.T, a, b interface{}) {
// Inner helper needs t.Helper() too
t.Helper()
if !reflect.DeepEqual(a, b) {
// If assertEqual missed t.Helper(), error points here
t.Errorf("not equal: %v != %v", a, b)
}
}
func TestAPIResponse(t *testing.T) {
// Errors in assertJSON or assertEqual point here
assertJSON(t, `{"id":1}`, `{"id":1}`)
}
If assertEqual misses t.Helper(), the error points to assertEqual. If both have it, the error points to assertJSON's caller. This emphasizes the stack filtering mechanism. You must audit the entire call chain. A single missing call breaks the attribution for the whole chain.
Helpers can call helpers. Every link in the chain needs t.Helper().
Pitfalls and runtime behavior
The compiler does not enforce t.Helper(). You can forget it and the code compiles fine. The only feedback is the confusing error output. You must rely on code review or habit to ensure helpers are marked.
If you pass a nil *testing.T to a helper, calling t.Helper() triggers a nil pointer dereference panic. The runtime stops with panic: runtime error: invalid memory address or nil pointer dereference. Always check for nil if the helper might be called outside tests, or document that the helper requires a valid t. A common pattern is to accept *testing.T and assume it is valid, since test functions always provide a non-nil instance.
t.Helper() only affects failures reported by the same *testing.T instance. It does not change the stack trace for panics or logs from other packages. If a helper calls a function that panics, the panic stack trace includes all frames. t.Helper() does not filter panics. It only filters errors reported via t.Errorf, t.Fatalf, t.Log, and related methods.
Subtests have their own *testing.T instance. The helper flag is per-instance. If you pass a subtest's t to a helper, the helper must call t.Helper() on that instance. The flag does not propagate from the parent test.
func TestParent(t *testing.T) {
t.Run("child", func(t *testing.T) {
// This t is a new instance
// Helper must call t.Helper() on this instance
helper(t)
})
}
func helper(t *testing.T) {
// Marks this frame for the specific t instance passed in
t.Helper()
t.Errorf("fail")
}
The compiler won't save you. You must remember to call it.
Decision matrix
Use t.Helper() when a function wraps test assertions and you want failures to point to the caller. Use a plain function without t.Helper() when the function performs setup or teardown that should be visible in the stack trace if it fails. Use a subtest via t.Run when you need to isolate a case so it can be run independently with go test -run. Use t.Helper() inside a subtest helper when the helper is called from within t.Run and you want the error to point to the line inside the subtest closure. Avoid t.Helper() in production code. The testing package is not included in production builds, and the method exists only for test diagnostics.
Helpers make tests readable. t.Helper() makes failures readable.