Why a Go error holding a nil pointer is not nil
A Go interface value carries a dynamic type and a dynamic value. Why returning a nil pointer as an error makes err != nil true, safe return patterns, and the limits of checking.
Sometimes a function does not fail, yet the caller’s if err != nil is true. It happens when the function declares a variable of type *MyError, leaves it nil when nothing goes wrong, and returns it. The pointer is nil, but the value the caller receives as an error is not.
The Go FAQ has a dedicated entry for this: “Why is my nil error value not equal to nil?” This post explains the behaviour using only the rules in the language specification and the FAQ, and sets out how far it can be avoided. The reference is the Go language specification checked on 2026-10-15 (Language version go1.27, May 26, 2026). These rules are not new in any particular release; they follow from the basic definition of interface values.
An interface value contains a type too
The “Variables” section of the spec says that a variable of interface type has two types: the static type given in its declaration, and a dynamic type, which is the (non-interface) type of the value assigned to it at run time. If the assigned value is the predeclared identifier nil, there is no dynamic type, because nil has no type.
The spec’s own example shows the point directly:
var x interface{} // x is nil and has static type interface{}
var v *T // v has value nil, static type *T
x = 42 // x has value 42 and dynamic type int
x = v // x has value (*T)(nil) and dynamic type *T
On the last line v is a nil pointer, but x now holds the dynamic type *T together with the value (*T)(nil).
The FAQ gives the same idea as an implementation picture. Under the covers an interface consists of two elements, a type T and a value V, and an interface value is nil only when both V and T are unset. Store a nil *int in an interface and the inner type is *int whatever the pointer’s value (T=*int, V=nil), so the interface value is not nil. The FAQ calls this two-slot model schematic; what the spec requires is the concept of dynamic type and dynamic value.
What == nil actually asks
Under “Comparison operators”, two interface values are equal if they have identical dynamic types and equal dynamic values, or if both have value nil. In err == nil, the nil on the right is compared as a nil interface value with no dynamic type. So an err whose dynamic type is *MyError is not equal to nil, even when the pointer inside is nil.
Returning follows the same rule. “Return statements” requires each returned expression to be assignable to the corresponding element of the function’s result type, and “Assignability” says a value x is assignable to an interface type T if x implements T. Return a *MyError from a function whose result type is error, and the result is an error value with dynamic type *MyError.
The code that goes wrong
The example below has the same shape as the FAQ’s returnsError example. Run with Go 1.27, it prints false and then stops with a run-time panic: runtime error: invalid memory address or nil pointer dereference.
package main
import "fmt"
type MyError struct {
Op string
}
func (e *MyError) Error() string {
return e.Op + " failed"
}
func find(ok bool) error {
var p *MyError
if !ok {
p = &MyError{Op: "find"}
}
return p // returns a non-nil error even when ok is true
}
func main() {
err := find(true)
fmt.Println(err == nil) // false
fmt.Println(err.Error()) // run-time panic
}
find(true) returns the nil pointer p, but on the caller’s side the dynamic type of err is *MyError. So err == nil is false.
The panic on the last line is easy to misattribute. The “Selectors” section says that calling a method on a nil interface value causes a run-time panic, but err here is not a nil interface. (*MyError).Error is called normally with a nil receiver, and the panic comes from e.Op inside the method body: the same section says that evaluating a struct field through a nil pointer causes a run-time panic. The stack trace agrees: it points to main.(*MyError).Error, at the line return e.Op + " failed". A method that never dereferences its receiver could return a string without panicking. Either way, the caller treats a function that did not fail as if it had failed.
Named result parameters behave the same way. Inside func find() (err error), assigning a nil pointer with err = p and then writing return gives err the dynamic type *MyError at the point of assignment.
Safe return patterns
The FAQ’s fix is to return an explicit nil on success:
func find(ok bool) error {
if !ok {
return &MyError{Op: "find"}
}
return nil
}
The nil in return nil becomes a nil interface value when it is assigned to the result type error. No dynamic type is ever filled in.
The FAQ also recommends that functions returning errors use the error type in their signature rather than a concrete type such as *MyError. Its example is os.Open, which returns an error even though, when not nil, the value is always of concrete type *os.PathError. If a function returns a concrete pointer type, the problem comes back as soon as someone moves its result into an error variable.
If you already have a pointer that may be nil and must pass it on as an interface, check the pointer before converting:
func wrap(p *MyError) error {
if p == nil {
return nil
}
return p
}
p == nil compares pointers, and by the spec two pointer values are equal if both are nil. The check happens before the value becomes an interface, so it does what it looks like.
Checking on the receiving side, and its limits
Once you have received an interface value that holds a nil pointer, the checks available from the spec alone all require knowing the concrete type.
- Compare against the concrete type’s nil. The spec allows comparing a value
xof non-interface typeXwith an interface valuet; they are equal ift’s dynamic type is identical toXand its dynamic value equalsx. Soerr == (*MyError)(nil)istruein the example above. - Comma-ok type assertion. With
p, ok := err.(*MyError),okistrueif the dynamic type is*MyError, andpreceives the stored pointer, which you can then test withp == nil. If the assertion does not hold,okisfalse,pis the zero value of*MyError, and no panic occurs. case nilin a type switch. Per the spec,case nilis selected only when the switch expression is a nil interface value. A value whose dynamic type is*MyErrorgoes tocase *MyError, notcase nil.
Each of these names one target type. The spec has no operator that asks, in general, “does this interface hold a nil of any pointer type?” This post stays within the spec and the FAQ, so checks using the reflect package are out of scope. In practice it is more reliable to stop a nil pointer from entering an interface on the sending side than to filter it out on the receiving side.
Summary
- An interface value carries a dynamic type and a dynamic value, and it is nil only when neither is set.
- Assigning or returning a nil pointer as an interface fills in the dynamic type, so
== nilisfalse. - On the success path, write
return nilexplicitly, and give error-returning functions theerrortype in their signature. - Check a possibly nil pointer before it becomes an interface. Afterwards, the comparisons, type assertions and type switches covered here can only catch it if you name the concrete type.
The FAQ’s closing sentence puts it briefly: if any concrete value has been stored in the interface, the interface will not be nil.
Sources
- The Go Programming Language Specification (Language version go1.27, May 26, 2026), checked 2026-10-15: https://go.dev/ref/spec
- Go FAQ, “Why is my nil error value not equal to nil?”, checked 2026-10-15: https://go.dev/doc/faq