A Go program can print an error that looks empty, enter an if err != nil branch, and still be holding a nil pointer underneath. This behavior surprises developers because it seems to violate the simple rule that “nil means no value.”

The rule is still consistent. The missing piece is that an interface value has two parts: a dynamic type and a dynamic value. An interface is nil only when neither part is set.

That distinction matters most with error, because error is an interface. A function can accidentally return a typed nil pointer as an error, producing a non-nil interface that callers treat as a real failure.

The useful mental model is:

nil interface:
dynamic type  = none
dynamic value = none

interface holding a nil *MyError:
dynamic type  = *MyError
dynamic value = nil

Both cases contain a nil-looking value, but only the first interface compares equal to nil.

Start with the smallest surprising example

Consider this program:

package main

import "fmt"

type MyError struct{}

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

func main() {
	var pointer *MyError = nil
	var err error = pointer

	fmt.Println(pointer == nil) // true
	fmt.Println(err == nil)     // false
}

The pointer is nil. The interface is not.

When pointer is assigned to err, Go records the concrete dynamic type *MyError in the interface. The dynamic value stored alongside that type is nil.

That gives the interface enough type information to be different from a completely empty interface value.

This is not an implementation accident that application code should depend on indirectly. The Go language specification defines interface variables as having a dynamic type, and it defines two interface values as equal when they have identical dynamic types and equal dynamic values, or when both interface values are nil.

Think of an interface as type plus value

A variable declared as an interface starts with its zero value:

var err error

fmt.Println(err == nil) // true

At this point, no concrete value has been assigned. There is no dynamic type and no dynamic value.

Now compare that with a typed pointer:

var parseErr *ParseError = nil
var err error = parseErr

Conceptually:

parseErr
static type: *ParseError
value:       nil

err
static type: error
dynamic type: *ParseError
dynamic value: nil

The interface is carrying real information: it knows that its dynamic type is *ParseError.

That is why err == nil is false.

Nil belongs to several kinds of types

Go uses nil as the zero value for pointer, function, slice, map, channel, and interface types. But “contains a nil value” does not mean “the surrounding interface is nil.”

For example:

var items []string = nil
var value any = items

fmt.Println(items == nil) // true
fmt.Println(value == nil) // false

The interface has dynamic type []string and dynamic value nil, so the interface itself is non-nil.

This distinction also applies to nil maps, channels, functions, and pointers stored inside interfaces.

The most common bug appears in error returns

The classic failure mode is a function whose concrete error pointer starts as nil:

type ValidationError struct {
	Field string
}

func (e *ValidationError) Error() string {
	return "invalid field: " + e.Field
}

func validate(name string) *ValidationError {
	if name == "" {
		return &ValidationError{Field: "name"}
	}
	return nil
}

So far, validate returns a concrete *ValidationError. A caller might wrap it in a function returning error:

func check(name string) error {
	return validate(name)
}

For valid input, validate(name) returns (*ValidationError)(nil). Converting that value to error produces a non-nil interface.

The result is surprising:

err := check("alice")

fmt.Println(err == nil) // false

The program now reports an error even though validation succeeded.

Return a nil interface when there is no error

The preferred fix is to make the success path return the untyped identifier nil directly from a function whose result type is error:

func check(name string) error {
	if err := validate(name); err != nil {
		return err
	}
	return nil
}

Now the success path constructs no concrete error value at all. The returned error interface has neither a dynamic type nor a dynamic value, so it compares equal to nil.

An even simpler design is often better: make the lower-level function return error itself if callers do not need the concrete pointer type.

func validate(name string) error {
	if name == "" {
		return &ValidationError{Field: "name"}
	}
	return nil
}

This makes the intended contract explicit: callers care whether validation failed, not whether the result variable happens to be a *ValidationError.

When callers need structured details, they can still expose those details through the concrete error value and inspect it with errors.As.

Why returning a concrete error pointer is risky

A signature such as this is legal:

func validate(name string) *ValidationError

It may even be useful internally when every caller genuinely needs that exact type.

The risk appears when the result crosses an interface boundary. Any conversion of a typed nil pointer to error or any creates a non-nil interface.

That means a function like this deserves extra scrutiny:

func run() error {
	return validate("alice")
}

The source looks like it merely forwards the error. Semantically, it converts a concrete pointer value into an interface value.

If the pointer is nil, that conversion changes the result of a nil comparison.

A practical guideline is:

If absence is represented by nil and callers conceptually consume an interface, return the interface type from the API boundary.

This reduces the number of places where typed nil values can be accidentally boxed into interfaces.

Methods can still be called through a non-nil interface

An interface holding a nil pointer still has a dynamic type, so a method call can dispatch to that type’s method.

Consider:

type Counter struct {
	Value int
}

func (c *Counter) String() string {
	if c == nil {
		return "<nil counter>"
	}
	return fmt.Sprintf("%d", c.Value)
}

func main() {
	var counter *Counter
	var text fmt.Stringer = counter

	fmt.Println(text == nil) // false
	fmt.Println(text.String()) // <nil counter>
}

The interface is non-nil, and method dispatch selects (*Counter).String. The receiver passed to the method is nil.

This is valid when the method intentionally handles a nil receiver.

But if the method dereferences the receiver without checking, it can panic:

func (c *Counter) ValueString() string {
	return strconv.Itoa(c.Value)
}

Calling that method through an interface containing (*Counter)(nil) attempts to access a field through a nil pointer.

The important separation is:

  • interface dispatch depends on the dynamic type;
  • safety inside the method depends on what the method does with its receiver.

Do not assume that a non-nil interface guarantees a non-nil pointer receiver.

A nil interface has no method target

A truly nil interface is different:

var text fmt.Stringer = nil

fmt.Println(text == nil) // true

There is no dynamic type available for method dispatch.

Attempting to call an interface method on such a value causes a run-time panic because there is no concrete method implementation to invoke.

This gives two different failure modes that can look similar in code:

nil interface
-> no dynamic type
-> method call cannot dispatch

interface holding nil *T
-> dynamic type is *T
-> method dispatch succeeds
-> method may handle nil receiver or panic while dereferencing it

Keeping those cases separate makes debugging much easier.

Type assertions expose the stored typed nil

A type assertion can recover the concrete typed nil from a non-nil interface:

var pointer *ValidationError = nil
var err error = pointer

validationErr, ok := err.(*ValidationError)

fmt.Println(ok)                   // true
fmt.Println(validationErr == nil) // true

The assertion succeeds because the interface’s dynamic type is *ValidationError.

The resulting pointer is nil because that was the dynamic value stored in the interface.

This is a useful diagnostic technique. It demonstrates that these two statements can both be true:

err != nil
validationErr == nil

They are comparing values of different types with different contents.

Reflection can detect some nil dynamic values, but use it carefully

Generic infrastructure sometimes needs to inspect values whose concrete type is not known in advance. The reflect package can report whether certain reflected values are nil.

For example:

func isNilLike(value any) bool {
	if value == nil {
		return true
	}

	v := reflect.ValueOf(value)

	switch v.Kind() {
	case reflect.Chan, reflect.Func, reflect.Map,
		reflect.Pointer, reflect.Slice, reflect.Interface:
		return v.IsNil()
	default:
		return false
	}
}

This handles both a nil interface and interfaces containing nil values of nil-capable kinds.

However, this should not become the default fix for typed-nil bugs.

Reflection adds complexity, and Value.IsNil panics when called on a kind that cannot be nil. The kind check above is therefore essential.

For ordinary application APIs, it is usually clearer to fix ownership and return-type boundaries so callers do not need a universal “nil-like” predicate.

Use reflection when the problem is genuinely generic, such as framework plumbing, serialization, validation utilities, or diagnostic tooling.

Interface comparisons have another important edge case

Interfaces are comparable, but comparing two interface values may panic if their identical dynamic type is not comparable.

For example:

var left any = []int{1, 2}
var right any = []int{1, 2}

fmt.Println(left == right) // panic

Slices are not comparable except against nil. When both interface values contain slices of the same dynamic type, equality requires comparing the dynamic values, and that comparison is not defined for slices.

This does not affect a direct comparison between an interface and nil:

var value any = []int(nil)

fmt.Println(value == nil) // false

That comparison is safe and false because the interface has a dynamic type.

The broader lesson is that interface equality depends on the comparability of the values stored inside it. interface == nil is a special, common case, but arbitrary interface-to-interface equality deserves more care.

Avoid “fixing” typed nil with string checks

A dangerous workaround is to inspect formatted output:

if fmt.Sprint(err) == "<nil>" {
	// treat as no error
}

This is not a reliable nil check.

Formatting is controlled by interfaces such as fmt.Stringer and error, and a method can return any text it chooses. A real error might render as "<nil>", and a typed nil receiver might panic or produce different output.

Use the type system and interface semantics directly:

if err == nil {
	// no error interface
}

Then design the producer so successful execution really returns a nil error interface.

Avoid returning typed nil through interface constructors

Helper functions can hide the same problem:

func asError(err *ValidationError) error {
	return err
}

The helper name suggests a harmless conversion, but asError(nil) returns a non-nil interface.

A safer helper must decide explicitly whether nil means no error:

func asError(err *ValidationError) error {
	if err == nil {
		return nil
	}
	return err
}

This same rule applies to factory functions returning any, callback interfaces, repository abstractions, optional strategies, and test doubles.

Whenever a concrete nil-able type becomes an interface, decide whether typed nil is a meaningful value or should become a nil interface.

Typed nil can be intentional

Not every non-nil interface containing a nil pointer is a bug.

A method may deliberately support nil receivers because the nil state has domain meaning:

type Logger struct {
	Prefix string
}

func (l *Logger) Log(message string) {
	if l == nil {
		return
	}
	fmt.Println(l.Prefix + message)
}

An interface holding (*Logger)(nil) can act as a no-op logger if that contract is deliberate and documented.

Still, this design has trade-offs. Readers may reasonably expect an interface comparison with nil to reveal whether a usable object exists. A nil-receiver convention makes that assumption false.

Prefer a concrete no-op implementation when explicit behavior is clearer:

type NopLogger struct{}

func (NopLogger) Log(string) {}

Now the interface contains a real value with ordinary method semantics, and nil retains its usual meaning of “no interface value.”

Common mistakes

Returning a typed nil pointer as error

This is the most important case:

func work() error {
	var err *MyError
	return err
}

The returned interface is non-nil.

Prefer:

func work() error {
	// ...
	return nil
}

on the success path.

Checking only the underlying pointer during debugging

Logging pointer == nil does not tell you whether an interface created from that pointer is nil. Inspect the interface value that callers actually receive.

Assuming a non-nil interface means a safe receiver

The interface may contain a nil pointer. Methods with pointer receivers must either support that state intentionally or avoid receiving it.

Using reflection where API design would solve the problem

A generic nil detector can be useful, but it is often a symptom that concrete values and interfaces are crossing boundaries without a clear absence contract.

Comparing arbitrary interface values without considering comparability

An interface can contain a slice, map, or function. Interface-to-interface equality can panic when the dynamic values are not comparable.

When to use pointers behind interfaces

Pointers behind interfaces are completely normal in Go. Many types implement interfaces through pointer-receiver methods because methods mutate state, copying the value is undesirable, or the method set naturally belongs to the pointer type.

The issue is not “pointer inside interface.”

The issue is using a nil pointer to represent absence, then assuming the resulting interface also represents absence.

Pointers behind interfaces are a good fit when:

  • the pointer normally refers to a valid object;
  • nil receiver behavior is explicit if nil can occur;
  • producers return a true nil interface when the interface contract uses nil to mean “not present” or “no error.”

They are a poor fit when callers must repeatedly discover whether the interface contains a hidden typed nil before every use.

Keep absence semantics at the API boundary

The typed-nil problem becomes straightforward once you stop treating an interface as a single opaque slot.

An interface value carries both a dynamic type and a dynamic value. A nil pointer can therefore produce a non-nil interface because the dynamic type is still present.

For error returns, the practical rule is simple: return nil directly when there is no error. Avoid forwarding concrete nil error pointers across an interface boundary without checking them first.

For other interfaces, decide whether a nil underlying value is meaningful. If it is, document and handle that state deliberately. If it is not, normalize absence to a nil interface before returning it.

That keeps if value == nil aligned with the API’s meaning instead of forcing every caller to reason about a hidden typed nil.