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.
| Declaration | Dynamic interface content | value == nil |
|---|---|---|
| var pointer *ReadError | Not an interface; nil pointer | true |
| var empty error | No dynamic type or value | true |
| var boxed error = pointer | *ReadError with a nil pointer | false |
| var value any = []int(nil) | []int with a nil slice | false |
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.