What this usually means
The classic Go gotcha: an interface value is a two-word pair (type, value). When you assign a nil pointer of a concrete type (e.g., *MyStruct(nil)) to an interface variable, the interface's type word is set to *MyStruct and its value word is nil. The interface itself is NOT nil because the type word is non-nil. So a comparison like `if err != nil` evaluates to true (the interface is not nil), even though the underlying pointer is nil. This usually happens when functions return a named error variable or a pointer type that is nil, wrapped in an interface return type.
The first ten minutes — establish facts before touching code.
- 1Check the concrete type of the interface with `fmt.Printf("%T\n", iface)` or `reflect.TypeOf(iface)`. If it shows a non-nil type, the interface is not nil.
- 2Print the interface value with `%#v` to see the underlying type and pointer: `fmt.Printf("%#v\n", iface)`
- 3Use `reflect.ValueOf(iface).IsNil()` to check if the underlying pointer is nil, but be careful: if the interface itself is nil, it panics.
- 4Add a debug assertion: `if iface != nil && reflect.ValueOf(iface).IsNil() { log.Println("interface is non-nil but holds nil pointer") }`
- 5Inspect the function return: ensure that a nil pointer of a concrete type is not returned directly as an interface. Instead, return nil interface explicitly.
- 6Run `go vet` on your code: it may catch some cases of comparing interface with nil when the concrete type is a pointer.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchFunctions that return an interface type (e.g., `error`, `io.Reader`) and use a named return variable of a concrete pointer type
- searchCode that assigns a pointer variable (e.g., `var p *MyStruct`) to an interface variable without explicit nil check
- searchJSON unmarshaling: `json.Unmarshal` into a struct with interface fields
- searchFactory functions or constructors that return interfaces
- searchAny place where a nil pointer is cast to an interface, e.g., `var err error = (*MyError)(nil)`
- searchDeferred functions that check for nil interface before calling methods on it
Practical causes, not theory. These are the things you will actually find.
- warningReturning a named pointer variable that is nil, but the function signature returns an interface: the interface gets the non-nil type info.
- warningAssigning a nil pointer of a concrete type to an interface variable directly, e.g., `var err error = (*MyError)(nil)`
- warningJSON unmarshaling into a pointer field: if the JSON field is missing, the pointer stays nil, but the interface field (if any) is non-nil.
- warningUsing a generic interface type as a return type in a factory, where the concrete type is a pointer and nil is returned as that pointer.
- warningRefactoring a function that used to return a concrete pointer to return an interface without adjusting nil handling.
- warningError handling that uses `errors.New` or `fmt.Errorf` correctly but later wraps the error in a custom type pointer that is nil.
Concrete fix directions. Pick the one that matches your root cause.
- buildAlways return a nil interface explicitly instead of a nil pointer: `if p == nil { return nil }` instead of `return p`.
- buildUse concrete types in function signatures when possible; avoid returning interfaces unless necessary.
- buildWhen returning an interface, use a helper that converts nil pointer to nil interface: `return ifaceNil[*MyError](p)` where ifaceNil is a generic function.
- buildFor JSON unmarshaling, check if the field exists before unmarshaling into a pointer field, or use a wrapper type.
- buildUse `reflect.ValueOf(iface).IsNil()` before using the interface value, but only after confirming the interface is non-nil.
- buildIn unit tests, compare the underlying pointer using `reflect.DeepEqual` or by type-asserting to the concrete type and comparing to nil.
A fix you cannot prove is a guess. Close the loop.
- verifiedAdd a test that returns a nil pointer and asserts the interface is nil: `if err != nil { t.Fatal("expected nil") }` should pass after fix.
- verifiedPrint the interface with `%#v` before and after fix to confirm the type is no longer set.
- verifiedRun a stress test with edge cases: missing JSON fields, empty slices, etc., and ensure no nil pointer dereferences.
- verifiedUse `go vet` and `staticcheck` to catch potential issues before they hit production.
- verifiedMonitor error rates and logs: after fix, the symptom of 'silent nil' should disappear.
- verifiedCover the fix with a regression test that explicitly tests the interface nil behavior.
Things that make this bug worse or harder to find.
- warningDon't use `if err != nil` when the error variable is of type interface{} or a custom interface that can hold a nil pointer.
- warningDon't assume that because the underlying pointer is nil, the interface is nil. They are different.
- warningAvoid using named return values for interface types; it complicates nil handling.
- warningDon't rely on `reflect.ValueOf(iface).IsNil()` without first checking that the interface is non-nil; it panics on nil interface.
- warningDon't use `if err == nil` as a guard for calling methods on the error; if err is non-nil but holds nil pointer, it panics.
- warningDon't ignore the warning from `go vet` about 'comparison of interface with nil' when the concrete type is a pointer.
Silent Failure in Payment Service Due to Interface Nil Pointer
Timeline
- 10:00Deploy new payment service version with custom error interface
- 10:15Alert: Payment processing rate drops 30%, no errors in logs
- 10:20Check logs: no error messages, but many responses have unexpected nil pointer
- 10:30Add debug logging: print error type and value before returning
- 10:35Deploy debug version, see: error interface holds *PaymentError(nil)
- 10:40Identify the bug: function returns a named *PaymentError that is nil
- 10:45Fix: return nil explicitly instead of the named variable
- 10:50Deploy fix, payment rate recovers to normal
We had a payment processing service written in Go that used a custom error interface. The function `ProcessPayment` returned an `error` interface, but internally it used a named return variable of type `*PaymentError`. The code looked like: `func ProcessPayment(ctx context.Context, req *PaymentRequest) (err error) { var pErr *PaymentError; ...; return pErr }`. When the payment succeeded, `pErr` was nil, but because the return type was `error`, the interface got the type `*PaymentError` and value nil. So `if err != nil` in the caller was true, and it tried to access `err.Error()` which panicked with nil pointer dereference because the underlying pointer was nil.
The symptom was a silent failure: the caller logged 'error' but the actual error message was never printed because `err.Error()` panicked. The panic was caught by a recovery middleware that logged a generic message, so we saw no actual error context. The payment was marked as failed even though it succeeded, causing a 30% drop in successful payments.
The fix was simple: change the function to return nil explicitly when there's no error: `if pErr == nil { return nil }`. This way the interface itself becomes nil. We also added a unit test that verifies the error interface is nil when the underlying pointer is nil. After deploying, the payment rate recovered. We also added a lint rule to catch returning named pointer variables from interface-returning functions.
Root cause
Returning a nil concrete pointer variable from a function that returns an interface causes the interface to be non-nil because the type information is set.
The fix
Explicitly return nil instead of the nil pointer variable. Also added a helper function that converts a nil pointer to a nil interface.
The lesson
Never return a named pointer variable from a function that returns an interface without checking for nil. Use explicit nil return or a wrapper.
In Go, an interface value is represented internally as a two-word structure: a pointer to the type information (itable) and a pointer to the data (value). When you assign a concrete value to an interface, both words are set. If the concrete value is a nil pointer of a type, the type pointer is non-nil (pointing to the type's metadata), but the data pointer is nil. This makes the interface itself non-nil because the type word is non-nil.
The critical insight: `iface == nil` checks only if both words are nil. So an interface with a nil pointer inside is NOT nil. This is why the comparison fails. Understanding this memory layout helps you predict when the gotcha occurs.
Pattern 1: Returning a named error variable. Example: `func foo() (err error) { var e *MyError; ...; return e }`. Here `e` is nil, but `err` becomes non-nil interface.
Pattern 2: Using a generic interface return type in a factory. Example: `func NewSomething() Something { return (*something)(nil) }` where `Something` is an interface.
Pattern 3: JSON unmarshaling into a struct with interface fields. If the JSON field is omitted, the pointer field stays nil but the interface field (if any) might be set to a non-nil interface with nil pointer.
Pattern 4: Using `errors.As` or type assertions on an interface that holds a nil pointer can cause panics if not handled.
You can use reflection: `if iface != nil && reflect.ValueOf(iface).IsNil() { // interface is non-nil but holds nil pointer }`. But beware: if `iface` is nil, `reflect.ValueOf(iface)` panics. So always check `iface != nil` first.
Alternatively, you can use a type switch to extract the concrete type and compare to nil: `switch v := iface.(type) { case *MyType: if v == nil { ... } }`.
The safest approach is to avoid the situation altogether by ensuring you never assign a nil pointer to an interface. Return nil explicitly.
Go vet can catch some cases: `go vet` flags interface comparisons with nil when the concrete type is a pointer? Actually, `go vet` does not catch this directly. But `staticcheck` has a check `SA4023` that warns about comparing interface with nil when the interface's concrete type is a pointer. Run `staticcheck -checks=SA4023 ./...`.
You can also use custom lint rules with `go-critic` or `revive`. For example, `revive` has a rule `unexported-return` that may help.
Code review: train your team to look for functions that return an interface but have a named return variable of a pointer type.
Frequently asked questions
Why does `err != nil` return true when the underlying pointer is nil?
Because `err` is an interface. An interface is nil only if both its type and value are nil. When you assign a nil pointer (e.g., `*MyError(nil)`) to an interface, the type is set to `*MyError`, making the interface non-nil even though the value is nil.
How can I check if an interface holds a nil pointer?
Use `reflect.ValueOf(iface).IsNil()`. But first ensure `iface` is not nil to avoid panic. Alternatively, use a type switch to extract the concrete type and compare to nil.
What is the best fix for this issue?
The best fix is to never return a nil pointer from a function that returns an interface. Instead, return nil explicitly. For example, change `return pErr` to `if pErr == nil { return nil } else { return pErr }`.
Does `errors.New` have this problem?
No, `errors.New` returns a non-nil interface with a concrete type `*errors.errorString` and a non-nil value. The problem occurs only when the concrete pointer itself is nil.
Can `go vet` detect this?
Standard `go vet` does not catch this. Use `staticcheck` with check `SA4023` or similar linters that analyze interface comparisons.