“I returned nil, so why is err != nil?” is famous enough to have its own entry in the official Go FAQ, and interviewers love it because it cannot be answered by memorising syntax. You have to know what an interface value physically contains. Candidates who answer well usually go on to explain method sets and when a type satisfies an interface, which is the natural follow-up.
The bug is also expensive in practice: it turns successful calls into failures, and the log line often says <nil>, so engineers lose hours staring at an error that claims to be nothing. Examples assume Go 1.23.
Before you start
You should know how to declare an interface and implement it with methods, the difference between value and pointer receivers, and that the built-in error is just an interface with one method, Error() string. Familiarity with type assertions (v, ok := x.(T)) helps.
The short answer
An interface value is a pair: a dynamic type and a dynamic value. It compares equal to nil only when both halves are unset. If you store a nil pointer of a concrete type, such as a nil *MyError, in an error, the type half is *MyError, so the interface is not nil even though the pointer inside is. The fix is to return the literal nil on success and only convert a concrete error to the interface when one actually occurred.
How it works
Internally, a non-empty interface such as error is two words: a pointer to an itab (which records the dynamic type and its method table) and a pointer to the data. An empty interface, any, is also two words: a type pointer and a data pointer. Either way, the comparison x == nil checks that the type word is empty.
type MyError struct{ Op string }
func (e *MyError) Error() string { return e.Op + " failed" }
var p *MyError // typed nil pointer
var err error = p // interface: (type=*MyError, value=nil)
fmt.Println(p == nil) // true
fmt.Println(err == nil) // false
fmt.Printf("%T\n", err) // *main.MyErrorAssigning p to err is an implicit conversion that records *MyError as the dynamic type. Nothing about the conversion checks whether the pointer is nil, and it should not: a nil pointer can be a perfectly valid receiver, because methods with pointer receivers can be called on nil pointers as long as they do not dereference them.
Interface equality in general compares both halves. Two interfaces are equal when they hold identical dynamic types and equal dynamic values. If the dynamic types match but are not comparable, such as two slices, the comparison panics at run time.
Step-by-step walkthrough
Step 1: Print both halves when debugging
fmt.Printf("type=%T value=%v isNil=%t\n", err, err, err == nil)
// type=*main.MyError value=<nil> isNil=false%T shows the dynamic type. The value prints as <nil> here because Error() dereferences e.Op and panics on the nil pointer; fmt catches panics from nil receivers and prints <nil> instead. That combination of “not nil” and a log line saying <nil> is the fingerprint of this bug.
Step 2: Reproduce the trap in a function
func validate(name string) error {
var verr *MyError
if name == "" {
verr = &MyError{Op: "validate"}
}
return verr // always non-nil as an error, even when name is valid
}The intent is “return the error if there is one”. But verr has a concrete type, and returning it converts to error on every path.
Step 3: Return the literal nil on success
func validate(name string) error {
if name == "" {
return &MyError{Op: "validate"}
}
return nil // untyped nil: both halves empty
}The general rule: declare variables that will be returned as the interface type (var err error), not as a concrete pointer type, and avoid exported functions whose result type is a concrete error pointer like func Parse() *ParseError. Callers assign those results to error variables and inherit the trap.
Step 4: Check method sets for satisfaction
The same “type is part of the value” idea explains which types satisfy an interface. The method set of T contains only value-receiver methods; the method set of *T contains both value and pointer receivers.
var _ error = (*MyError)(nil) // compiles: *MyError has Error
var _ error = MyError{} // compile error: method Error has pointer receiverThese var _ I = ... lines are a common idiom for asserting satisfaction at compile time, and they cost nothing at run time.
Step 5: Detect a typed nil only when you must
Sometimes you receive an any from code you do not control. Reflection can look inside:
func isNilValue(v any) bool {
if v == nil {
return true
}
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Pointer, reflect.Map, reflect.Slice, reflect.Func, reflect.Chan, reflect.Interface:
return rv.IsNil()
}
return false
}Use this sparingly, at boundaries. Inside your own code, fixing the producer is always better than defending every consumer.
Worked scenario
A team adds request validation to an HTTP API. Within minutes of deploying, every request gets a 400 response, valid or not, and the log says validation failed: <nil>.
type ValidationError struct {
Field string
}
func (e *ValidationError) Error() string { return "invalid " + e.Field }
func validateOrder(o Order) *ValidationError { // concrete return type
if o.Quantity <= 0 {
return &ValidationError{Field: "quantity"}
}
return nil
}
func handle(w http.ResponseWriter, r *http.Request) {
var o Order
// decode body into o ...
var err error = validateOrder(o) // typed nil converted to error
if err != nil {
log.Printf("validation failed: %v", err)
http.Error(w, "invalid order", http.StatusBadRequest)
return
}
// process the order ...
}validateOrder correctly returns a nil *ValidationError for valid orders, but the handler stores it in an error, which is now non-nil, so every request takes the failure branch. The log prints <nil> because Error() dereferences e.Field and fmt recovers from that panic. Any code that called err.Error() directly would panic instead, and net/http would recover it and drop the connection.
The fix changes the signature so the function owns the conversion:
func validateOrder(o Order) error {
if o.Quantity <= 0 {
return &ValidationError{Field: "quantity"}
}
return nil
}
if err := validateOrder(o); err != nil {
var verr *ValidationError
if errors.As(err, &verr) {
http.Error(w, verr.Error(), http.StatusBadRequest)
return
}
http.Error(w, "internal error", http.StatusInternalServerError)
return
}Callers that need the concrete type use errors.As, which also works through wrapping. Static analysis helps too: staticcheck’s SA4023 check reports some comparisons that can never be nil because of this pattern.
Common mistake
- “The interface compares the pointer to nil.” It compares the type word first; a typed nil fails the check.
- “It’s a Go bug.” It is deliberate: a nil pointer of a concrete type is a valid value with methods.
- Returning concrete error types from functions to “be more specific”. Return
errorand let callers useerrors.As. - Thinking
MyError{}satisfies an interface whose method has a pointer receiver. Only*MyErrordoes. - Fixing it with
reflecteverywhere instead of fixing the function that returns the typed nil.
Verify the behavior
func TestValidateReturnsUntypedNil(t *testing.T) {
var err error = validateOrder(Order{Quantity: 1}) // store it the way callers do
if err != nil {
t.Fatalf("valid order: got non-nil error of type %T", err)
}
err = validateOrder(Order{Quantity: 0})
var verr *ValidationError
if !errors.As(err, &verr) || verr.Field != "quantity" {
t.Fatalf("invalid order: got %v", err)
}
}The explicit var err error matters. With the broken *ValidationError signature, err := validateOrder(...) would infer the concrete pointer type, the nil comparison would pass, and the test would hide the bug. Storing the result as error, as real callers do, makes the broken version fail with a message naming the *ValidationError type. Run it with go test -run TestValidateReturnsUntypedNil -v.
Follow-up questions
Can you call a method on a nil pointer? Yes, if the method has a pointer receiver and does not dereference it. Some types use this deliberately, for example to make a nil tree behave as empty.
What is the difference between any and interface{}? None; any is an alias added in Go 1.18.
Why does comparing two interfaces sometimes panic? When both hold the same uncomparable dynamic type, such as a slice or map, == panics at run time.
How do you make an interface value nil again? Assign the untyped nil to it, which clears both halves.
Interview exercise
What does this program print, and where does it stop?
var w io.Writer
fmt.Println(w == nil)
var buf *bytes.Buffer
w = buf
fmt.Println(w == nil)
w.Write([]byte("hello"))
fmt.Println("written")Answer and reasoning
It prints true, then false, then panics with runtime error: invalid memory address or nil pointer dereference, so written never appears. The first w is a zero-valued interface with no type. Assigning the nil *bytes.Buffer gives it the dynamic type *bytes.Buffer, so it is no longer nil. Calling Write dispatches to (*bytes.Buffer).Write with a nil receiver, and that method touches the buffer’s fields, which dereferences the nil pointer. A strong answer names both halves of the interface and explains that method dispatch works on the dynamic type even when the value is nil.
Continue learning
Practise with the Go interview questions and the Go MCQs. Related notes: Go error wrapping with errors.Is and errors.As, TypeScript structural typing for a different take on implicit interfaces, and Java equals and hashCode. Primary sources: the Go FAQ entry on nil errors, the interface types section of the specification and Effective Go on interfaces.