Why a nil Error Is Not nil in Go

Why returning a nil pointer of a custom error type makes err != nil true, how to spot the bug, and the simple rule that prevents it.

This bug surprises almost every Go developer once. A function returns what looks like a nil error, yet the caller's if err != nil check is true, and the program reports a failure that never happened. The cause is how interface values work, and the fix is a one-line habit.

The Bug in Action

package main

import "fmt"

type MyError struct{ Msg string }

func (e *MyError) Error() string { return e.Msg }

func check(ok bool) error {
	var e *MyError // nil pointer
	if !ok {
		e = &MyError{Msg: "check failed"}
	}
	return e // returns a non-nil error even when e is nil
}

func main() {
	err := check(true)
	fmt.Println("err == nil:", err == nil)
	fmt.Printf("dynamic type: %T, value: %v\n", err, err)
}

Output:

err == nil: false
dynamic type: *main.MyError, value: <nil>

check(true) should succeed, and e really is a nil pointer. Still, the caller sees err == nil as false.

Why It Happens

An interface value, including error, is made of two parts: a type and a value. It is only equal to nil when both parts are empty.

var err error            type: (none)       value: (none)   -> err == nil is true
err = (*MyError)(nil)    type: *MyError     value: nil      -> err == nil is false

When check returns e, Go converts the *MyError pointer into an error interface. The interface now remembers its type, *MyError, even though the pointer inside is nil. Because the type part is filled in, the interface is not nil.

You can think of the interface as a labeled box. An empty box with no label is nil. A box labeled "MyError" that has nothing inside is still a labeled box, so it does not count as "no box".

It can also crash:

If the caller treats this error as real and calls err.Error(), the method runs with a nil receiver and reads e.Msg, which panics with invalid memory address or nil pointer dereference. So the bug can show up either as a false failure or as a crash inside the error handling itself.

The Fix: Return a Literal nil

Never return a variable of a concrete error type through an error result. Return nil explicitly on success:

package main

import "fmt"

type MyError struct{ Msg string }

func (e *MyError) Error() string { return e.Msg }

func check(ok bool) error {
	if !ok {
		return &MyError{Msg: "check failed"}
	}
	return nil
}

func main() {
	fmt.Println("ok case, err == nil:", check(true) == nil)
	fmt.Println("fail case:", check(false))
}

Output:

ok case, err == nil: true
fail case: check failed

Writing return nil directly in a function whose result type is error creates an interface with no type and no value, which is a true nil.

The Same Trap With a Variable Declared as the Concrete Type

The bug also appears when a helper returns the concrete pointer type and you store its result in an error variable:

package main

import "fmt"

type MyError struct{ Msg string }

func (e *MyError) Error() string { return e.Msg }

func validate(s string) *MyError { // returns concrete type
	if s == "" {
		return &MyError{Msg: "empty"}
	}
	return nil
}

func main() {
	var err error = validate("hello")
	fmt.Println("looks nil, but err == nil:", err == nil)
}

This prints looks nil, but err == nil: false. validate returned a nil *MyError, and assigning it to err filled in the type part.

Keep error results typed as error:

Functions that can fail should declare their result as error, not as *MyError. Callers then always receive a proper interface value, and your custom type can still be recovered with errors.As.

How to Spot It

If an error prints as <nil> but err != nil is true, you have found this bug. Printing the type with %T confirms it:

fmt.Printf("%T %v\n", err, err) // *main.MyError <nil>

The nilness analyzer in gopls and linters like staticcheck warn about some forms of this mistake, but the reliable protection is the habit: return nil literally, and declare error results as error.

You are safe when:

Every success path in your functions ends with a literal return nil (or return value, nil), and no function that can fail has a concrete error type as its result. With those two habits, this trap cannot reach you.

Summary

An error is only nil when both its type and value are empty. Returning a nil pointer of a custom error type fills in the type, so err != nil becomes true even though nothing failed. Return a literal nil on success, declare failing functions with an error result instead of a concrete pointer type, and use %T to confirm the bug when an error prints as <nil> but is not nil.