Skip to content
Languages

Go Nil Interface vs Typed Nil: Why err != nil

A Go function can return a nil pointer as an error and still make the caller enter if err != nil. The important distinction is between an interface with no dynamic type and an interface containing a typed nil pointer. This guide reproduces the bug, explains the comparison, and fixes the successful return path.

By Published

5 min readEditorial analysisUpdated
  • Go
  • interfaces
  • nil
  • error handling
What to remember

An interface equals nil only when it has neither a dynamic type nor a dynamic value. Returning a nil *ReadError as error retains the type. Return the literal nil on success.

Reproduce a non-nil error containing a nil pointer

Save this complete program as main.go and run go run main.go. The first line is true true false. pointer is nil, and empty is a nil interface, but assigning pointer to boxed gives the interface a dynamic type: *main.ReadError.

The second line is *main.ReadError <nil ReadError>, and the third line is false. The example's Error method deliberately accepts a nil receiver so that formatting remains safe. The printed message is not an equality test; a string representation can obscure the type information that determines the comparison.

package main

import "fmt"

type ReadError struct{ Path string }

func (e *ReadError) Error() string {
    if e == nil { return "<nil ReadError>" }
    return "cannot read " + e.Path
}

func broken() error {
    var issue *ReadError
    return issue
}

func main() {
    var pointer *ReadError
    var empty error
    var boxed error = pointer
    fmt.Println(pointer == nil, empty == nil, boxed == nil)
    fmt.Printf("%T %v\n", boxed, boxed)
    fmt.Println(broken() == nil)
}

Separate the interface from the value inside it

Think of the dynamic type and value as a model for reasoning, not a promise about the runtime's physical memory layout. Converting a typed pointer to an interface preserves its concrete type even when the pointer's value is nil.

Comparing an interface directly with nil is safe. Comparing two interfaces is a different question: if their equal dynamic types are not comparable, such as a slice type, the comparison can panic. Avoid expanding the nil rule into a claim that every possible interface comparison is safe.

Which values compare equal to nil?
DeclarationDynamic interface contentvalue == nil
var pointer *ReadErrorNot an interface; nil pointertrue
var empty errorNo dynamic type or valuetrue
var boxed error = pointer*ReadError with a nil pointerfalse
var value any = []int(nil)[]int with a nil slicefalse

Fix the function's success contract

The caller should not need reflection to determine whether the operation succeeded. Keep the public return type error, and return a literal nil on the successful branch. Create a concrete error only when there is an actual failure to report.

This program prints true, then cannot read config.json. It also removes the need for a nil receiver in Error because its failure branch constructs a real value. Add a regression assertion around the successful call; testing only the failure text would miss the original bug.

package main

import "fmt"

type ReadError struct{ Path string }
func (e *ReadError) Error() string { return "cannot read " + e.Path }

func read(fail bool) error {
    if fail { return &ReadError{Path: "config.json"} }
    return nil
}

func main() {
    fmt.Println(read(false) == nil)
    fmt.Println(read(true))
}

Does calling a method on a typed nil always panic?

No. A method with a pointer receiver can be called with a nil receiver, and its implementation may explicitly handle that case, as the first program does. If the method dereferences the receiver without checking it, it can panic. A genuinely nil interface has no concrete method implementation to dispatch to.

This is why changing Error to print a friendly message is not a fix for the return contract. The error interface would still compare unequal to nil. Likewise, checking the displayed text for a nil-looking value turns presentation into control flow and breaks as soon as formatting changes.

A practical checklist for debugging err != nil

First inspect the concrete type with %T and inspect the function's return branches. Look for a declared pointer variable that is returned through an interface without an explicit success check. Then add separate assertions for success and failure. Test the caller's observable result, not the runtime's implementation details.

For a follow-up interview exercise, replace error with a small custom interface and predict the result before running the program. The same issue can appear in factories that return interfaces. Make an explicit decision about whether the API returns no implementation or an implementation whose internal state is empty.

  • Trace the conversion at the return statement.
  • Use a literal nil for absence at the interface boundary.
  • Keep nil receiver support only when it is part of the method's intended contract.
  • Do not use errors.Is(err, nil) as a generic unboxing fix; it does not turn a typed nil into a nil error.

Quick answers

Frequently asked questions

Does any solve the typed nil problem?

No. any is an alias for the empty interface. Assigning a typed nil pointer or slice to it still preserves the dynamic type, so the resulting interface is not nil.

Should I use reflection to check every error?

Usually the fix belongs in the function returning the error. Reflection adds kind-specific handling and can itself panic when used incorrectly; it should not replace a clear success contract.

Why does printing nil not prove err == nil?

Formatting can invoke methods or display the contained value. Equality against nil depends on whether the interface has a dynamic type and value, not on its printed text.

Source notes

References and review policy

Information checked on October 21, 2026. Section links identify sources for factual claims and technical explanations. Interpretations, practice scenarios and preparation recommendations are RecallDeck’s editorial work.

From reading to recall

Practice the full interview loop.

RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.

Start studying

Keep going

Languages105 min

Top 100 Go Interview Questions and Answers

Prepare with 100 Go interview questions and detailed answers on slices, interfaces, concurrency, runtime, testing, distributed systems, and reliability.

100 detailed answers