Fix

"error is not nil but has a nil value" (Nil Interface Gotcha)

Fix the Go nil interface error by checking if the underlying value is nil before calling methods on the error.

The box has a label, but nothing inside

You write a function that returns an error. You check if err != nil. You feel safe. You call err.Error() to log the message. The program panics. The stack trace points to your error check. The error variable is definitely not nil, yet it has no value. This is the nil interface gotcha, and it trips up almost every Go developer at least once.

The panic message reads runtime error: invalid memory address or nil pointer dereference. The runtime tried to call a method on a nil pointer. The interface told the runtime there was a value there. The interface was lying.

How interfaces actually work

In Go, an interface is not just a value. It is a pair of things: a concrete type and a value of that type. When you assign nil to an interface variable, both the type and the value are nil. The interface itself is nil. When you assign a concrete value, both slots fill up. The interface is non-nil.

The gotcha happens when the type slot is filled, but the value slot is nil. The interface knows it holds an error type, but the error value inside is empty. The interface is not nil. The value is nil.

Think of a labeled box. The label says "Error". The box is sitting on the table. The box is not empty of existence; it has a label. But inside the box, there is nothing. You see the box, so you assume there is something inside. You reach in and grab nothing. Your hand hits the bottom. The box exists, but the content is nil.

Interfaces are pairs. Check the type, not just the value.

Minimal example of the trap

This code reproduces the bug. The function returns a typed nil pointer. The caller checks for nil, passes the check, and crashes.

package main

import "fmt"

// MyError implements the error interface.
type MyError struct{}

// Error returns the error message.
func (e *MyError) Error() string {
    return "something went wrong"
}

// doWork returns an error.
func doWork() error {
    // Return a typed nil pointer.
    // This creates a non-nil interface with a nil value.
    return (*MyError)(nil)
}

func main() {
    err := doWork()

    // err != nil is true because the interface has a type.
    if err != nil {
        // This panics because the underlying value is nil.
        // runtime error: invalid memory address or nil pointer dereference
        fmt.Println(err.Error())
    }
}

The compiler sees (*MyError)(nil). It knows *MyError implements the error interface. It packs the type *MyError and the value nil into the interface. The interface variable err now holds a pair: type is *MyError, value is nil. The check err != nil compares the interface pair against a fully nil interface. The types differ. The comparison returns true. You enter the block. You call err.Error(). Go looks at the type, finds the method, and calls it with the value. The value is nil. The method tries to dereference the pointer. The runtime panics.

The compiler packs the type. The runtime dereferences the value. The mismatch crashes your program.

Why the language allows this

Go interfaces are dynamic. The type information is part of the interface value. This allows type assertions and type switches. You can ask an interface what concrete type it holds. The trade-off is that the interface can hold a nil value while remaining non-nil. The language prioritizes the ability to carry type information over preventing this specific edge case.

This design supports advanced patterns where a function returns a specific error type even on success, though this is rare and generally discouraged. The convention is clear: return plain nil on success. Typed nils break the convention and cause confusion.

Realistic scenario: the wrapper function

This bug often appears in wrapper functions or when returning custom error types. A developer wants to return a specific error type but forgets to check for nil before returning.

package main

import (
    "fmt"
    "net/http"
)

// CustomError implements error.
type CustomError struct {
    Code int
}

// Error returns the error message.
func (e *CustomError) Error() string {
    return fmt.Sprintf("error code: %d", e.Code)
}

// fetchData returns an error.
// BUG: Returns typed nil on success.
func fetchData() error {
    // Simulate success.
    // Returning (*CustomError)(nil) instead of nil.
    return (*CustomError)(nil)
}

// handleRequest processes an HTTP request.
func handleRequest(w http.ResponseWriter, r *http.Request) {
    err := fetchData()

    // The check passes because err is not nil.
    if err != nil {
        // Panic: runtime error: invalid memory address or nil pointer dereference
        fmt.Fprintln(w, err.Error())
        return
    }

    fmt.Fprintln(w, "OK")
}

The function fetchData returns (*CustomError)(nil) on success. The caller sees a non-nil error. The handler tries to log the error and crashes. The server returns a 500 error to the user. The bug is hidden in the return statement. The compiler cannot catch this. The interface contract allows a nil value inside a non-nil interface.

The bug hides in the return statement. The server crashes. The user sees a 500 error.

Pitfalls and bad fixes

You might see code that checks if err != nil && err.Error() != "". This is a bandage. It prevents the panic but masks the underlying problem. The function returned a typed nil when it should have returned a plain nil. The error handling logic is broken. The check err.Error() != "" assumes that a nil error has an empty string message. This is not guaranteed. A custom error type could return a non-empty string even when the value is nil, or it could panic for other reasons.

Checking the error message is fragile. It couples the code to the implementation details of the error type. It also fails if the error message contains whitespace. The correct fix is to ensure the function returns plain nil on success. If you control the function, change the return statement. If you do not control the function, use the standard library tools that handle nil values safely.

Bandages hide bugs. Fix the source or use the standard library.

Convention aside: return nil, not typed nil

Go conventions dictate that functions return nil on success. The type of nil is untyped. When you assign nil to an error return, the interface becomes fully nil. If you assign a typed nil, you break the convention. The community expects err == nil to mean success. Typed nils violate this expectation.

The mantra "accept interfaces, return structs" applies here. When returning an error, return a concrete struct value or plain nil. Do not return a pointer to a struct unless you need to modify the error later. Even then, return nil on success, not a typed nil pointer.

How to fix the caller

If you cannot change the source code that returns the typed nil, you must handle it in the caller. The standard library provides errors.Is and errors.As. These functions check for nil values before comparing or asserting types. They are safe to use with typed nils.

package main

import (
    "errors"
    "fmt"
)

type MyError struct{}

func (e *MyError) Error() string {
    return "something went wrong"
}

func doWork() error {
    return (*MyError)(nil)
}

func main() {
    err := doWork()

    // errors.Is handles nil values correctly.
    // It returns false if err is nil or has a nil value.
    if errors.Is(err, &MyError{}) {
        fmt.Println("Got MyError")
    } else {
        fmt.Println("No error or different error")
    }
}

errors.Is compares the error against a target. If the error interface is nil or contains a nil value, errors.Is returns false. This prevents the panic and handles the logic correctly. You can also use a type switch to inspect the concrete type and check for nil explicitly.

switch e := err.(type) {
case *MyError:
    if e == nil {
        // Handle typed nil case.
        return
    }
    // Handle non-nil MyError.
}

The type switch extracts the concrete value. You can then check if the value is nil. This gives you full control over the handling. Use this approach when you need to distinguish between a typed nil and a real error of the same type.

errors.Is handles the edge cases. Use it to keep your error logic safe.

Decision matrix

Use return nil when the function succeeds and you want to return a plain nil interface. Use return (*MyError)(nil) only when you intentionally want to return a non-nil interface with a nil value, which is almost never the right choice. Use errors.Is(err, target) when you need to compare errors safely, as errors.Is handles nil values correctly. Use a type switch switch err.(type) when you must inspect the concrete type and handle the nil case explicitly. Use err == nil check before calling methods, but remember this check fails for typed nils.

Where to go next