The nil interface trap in Go will bite you eventually
A *MyError pointer set to nil is nil. Assign it to an error interface, however, and err != nil is true. This catches plenty of Go developers out, particularly in error returns and test doubles.
The Go FAQ addresses this directly: an interface value is only nil when both its type and value are nil. The moment you assign a typed nil to an interface, the type information gets set. The interface stops being nil.
Here’s exactly how this happens, why Go works this way, and what you can do about it.
How interface values work in Go
It helps to think of an interface value as a pair: a dynamic type and a dynamic value. Russ Cox’s article on Go Data Structures: Interfaces explains the runtime representation in more detail. An interface is nil only when it has no dynamic type and no dynamic value.
Assigning a concrete typed nil to an interface gives that interface a dynamic type:
package main
import "fmt"
type MyError struct {
Message string
}
func (e *MyError) Error() string {
return e.Message
}
func mayFail(fail bool) error {
var err *MyError // nil pointer of type *MyError
if fail {
err = &MyError{Message: "something broke"}
}
return err // returns (*MyError)(nil), NOT a nil interface
}
func main() {
err := mayFail(false)
fmt.Println(err == nil) // false — this surprises people
fmt.Printf("type: %T, value: %v\n", err, err) // type: *main.MyError, value: <nil>
}
mayFail always returns a non-nil error, even on the success path. The variable err is a *MyError set to nil, but when it’s returned as an error interface, the interface carries the *MyError type information. So err != nil evaluates to true.
That’s the typed nil trap: the interface itself is not nil, even though its dynamic value is.
Where this shows up in practice
Error returns
The example above is the most common case. Any function that declares a concrete error type and returns it through an error interface risks this bug. Codebases with custom error types are especially prone. For more on error handling patterns, see Error Handling Best Practices in Go.
The fix is simple: return nil explicitly.
func mayFail(fail bool) error {
if fail {
return &MyError{Message: "something broke"}
}
return nil // explicit nil interface, not a typed nil
}
Now the caller’s if err != nil check works correctly.
Mock and test interfaces
This trap shows up in test code too. Say you have an interface for a dependency:
type UserStore interface {
GetUser(id string) (*User, error)
}
A mock might return (*CustomError)(nil) instead of a plain nil for the error. The calling code enters an error-handling branch it shouldn’t. If your mocks use concrete error types internally, always return an untyped nil on the success path.
Type assertions and wrapping
Type assertions, type switches, and errors.As work from the dynamic type, so they can successfully match *MyError while producing a nil *MyError value. Code that assumes a successful match always yields a usable pointer can then fail. Likewise, calling a method through the non-nil interface passes a nil receiver. Whether that panics depends on the method; this Error method dereferences the receiver, so it does:
err := mayFail(false) // non-nil interface, nil underlying pointer
if err != nil {
fmt.Println(err.Error()) // PANIC: nil pointer dereference
}
The err != nil guard passes, so your code calls Error() on a nil *MyError receiver. Crash.
How to debug it
When you suspect a typed nil, use fmt.Printf with %T and %v:
fmt.Printf("err type: %T, value: %v\n", err, err)
If you see something like type: *main.MyError, value: <nil>, that’s your culprit. The interface has a type but no value.
You can also use the reflect package for programmatic checks:
import "reflect"
func isNilInterface(i interface{}) bool {
if i == nil {
return true
}
v := reflect.ValueOf(i)
return v.Kind() == reflect.Ptr && v.IsNil()
}
This pointer-specific helper is useful for investigating this example. A truly generic helper must also handle nil-capable channel, function, interface, map, and slice values before calling IsNil, which panics for other kinds. Avoid using reflection to paper over an API that can return typed nils; fix the return path when you control it.
API patterns that avoid the trap
Return nil explicitly, every time
Don’t return a possibly nil concrete pointer through an interface when the success case means “no error”:
// Bad
func doWork() error {
var err *MyError
// ... maybe set err ...
return err
}
// Good
func doWork() error {
// ... if something fails ...
if somethingWrong {
return &MyError{Message: "failed"}
}
return nil
}
Don’t declare concrete error variables you return through interfaces
If you need a concrete error type, construct it at the failure return site. Declaring var err *ConcreteType at the top of a function and returning it at the bottom makes it easy for a typed nil to sneak through.
Use interface guards to catch mistakes early
Interface guards won’t prevent the nil trap directly, but they catch type mismatches at compile time. Good discipline when you’re working with custom types that implement interfaces.
Lint for it
The NilAway analyzer from Uber tracks nil flows and reports potential nil panics across functions and packages. It does not promise to find every nil bug, and its documentation warns that false positives and breaking changes are still possible, but it can add a useful check in CI.
Why Go works this way
This follows from Go’s interface model. Interfaces carry dynamic type information for method dispatch, type assertions, and type switches. If an interface only stored a value, those operations would not know which concrete type they were working with.
The tradeoff: nil semantics become less intuitive. The Go FAQ acknowledges this is confusing and recommends returning explicit nil values.
Understanding how defer interacts with return values is another area where Go’s mechanics can surprise you, especially when deferred functions modify named return values that include error interfaces.
The short version
The typed nil interface trap comes down to one thing: an interface is only nil when it has no type and no value. Assign a nil pointer of any concrete type to an interface, and it stops being nil.
The rules to avoid it:
- Return
nilexplicitly for success cases in functions that return interfaces. - Don’t return possibly nil concrete pointers through interface types.
- Use
fmt.Printf("%T, %v", err, err)when debugging suspicious nil checks. - Consider a nilness analyzer such as NilAway in CI.
The trap is well-documented and easy to avoid once you know the mechanics. The danger is that you can write Go for years before hitting it in a way that matters, and by then the pattern is already scattered across your codebase.