Your function returns nil on the success path, the caller checks err != nil, and the check fires anyway. What happened?
A Go interface is non-nil when it holds a concrete type, even if the concrete pointer value is nil. Returning a typed nil pointer as an error creates an interface value that fails err == nil checks.
What the interviewer is scoring
- Does the candidate know an interface value is a type and a value rather than one word
- Whether they can say why comparing the interface to nil is false when the pointer is nil
- That the fix is the declared return type, not an extra check at the call site
- Whether they recognise the same shape in a nil map read against a nil map write
- Does the candidate connect this to why errors.Is exists rather than comparing directly
Answer
Short answer
In Go, an interface value contains both a dynamic type and a value. If a function returns a typed nil pointer as error, the interface has a type even though the pointer is nil, so err != nil is true. Return a plain nil error instead of a typed nil pointer.
Keep interfaces explicit in the answer because that is the concept the interviewer is actually trying to test. A good interfaces explanation names the trade-off, the failure mode, and the evidence you would use before choosing. Use interfaces once more at the decision point so the answer reads as judgement rather than a detached example.
Keep interfaces explicit in the answer because that is the concept the interviewer is actually trying to test. A good interfaces explanation names the trade-off, the failure mode, and the evidence you would use before choosing.
The shape of the bug
It is almost always this, and once you have seen it once you spot it by the signature alone.
type ValidationError struct{ Field string }
func (e *ValidationError) Error() string { return "invalid " + e.Field }
// The bug is in the return type. This says "an error", and the value
// returned on success is a nil *ValidationError, which is not nothing.
func validate(input string) *ValidationError {
if input == "" {
return &ValidationError{Field: "input"}
}
return nil
}
func handle(input string) error {
// Assigning a nil *ValidationError into an error interface produces a
// non-nil interface holding a nil pointer.
return validate(input)
}
func main() {
if err := handle("fine"); err != nil {
// Reached. err != nil is true, and err.Error() panics.
}
}
Nothing here is a compiler bug or an obscure corner. Every line follows from what an interface value is.
An interface value is two words
A variable of a concrete pointer type holds one thing: an address, which may be zero. A variable of an interface type holds two: a pointer to type information, and a pointer to the data. The interface is nil only when both are unset — which is to say, when nothing has ever been assigned to it.
Assigning a nil *ValidationError sets the first word to *ValidationError and
the second to zero. The type word is populated, so the interface is not nil. It
is an interface that definitely holds something, and the something it holds
happens to be a nil pointer.
That is why err != nil is true. The comparison is asking about the interface,
not about the pointer inside it, and the interface is genuinely not empty.
Two consequences follow. Calling a method on it does not immediately panic —
method dispatch works fine, because the type word tells the runtime which
Error to call — and it panics only when that method dereferences its nil
receiver. And a method with a value receiver on a nil pointer panics before the
body runs at all, while a method that never touches the receiver works
perfectly, which is why this bug can lurk for months in code whose error type
happens to have a constant message.
Fix the signature, not the call site
The tempting fix is defensive and wrong:
// Wrong. It papers over one call site and leaves the trap for the next.
if err := handle(input); err != nil && err.(*ValidationError) != nil {
The correct fix is to stop producing the value. A function that reports failure
should return error, not a concrete pointer type:
// Returns the interface directly, so the success path returns a genuine
// nil interface rather than a typed nil wrapped in one.
func validate(input string) error {
if input == "" {
return &ValidationError{Field: "input"}
}
return nil
}
The rule generalises: never return a concrete error type from a function
whose result is going to be assigned to error. Linters catch the common
form, and knowing why the linter is right is what an interviewer is after.
If you genuinely need a concrete type — a constructor returning
*ValidationError for a caller that will inspect its fields — then the caller
must convert deliberately rather than assigning through an error return, and
the function must not be the one filling in an error slot.
The same mistake wearing other clothes
Go has a small family of these, all arising from a value being more structured than it looks.
A nil map reads fine and returns the zero value, and panics on write. So a struct with an uninitialised map field works in tests that only read it.
A nil slice appends fine, because append allocates when there is no
backing array. So nil and empty behave identically for most purposes, and
differ when you compare them or serialise them to JSON, where one becomes
null and the other [].
A nil channel blocks forever on both send and receive, which is either a
deadlock or, in a select, a deliberately disabled case — one of the few
places where the behaviour is a feature.
The unifying point is that Go's zero values are usable, which is a genuine strength, and the price is that "not initialised" and "deliberately empty" are frequently the same bit pattern.
Why this is on the interview
It is a good question because it cannot be answered from documentation you skimmed. A candidate who says "the interface has a type word" has a model of the runtime; a candidate who says "you should check for nil differently" has met the symptom and not the cause. It also has a real production shape — the wrapped error that is always non-nil turns every success into a logged failure, and the handler that panics on a nil receiver takes down a request that succeeded.
An interface is nil only when it holds no type. A nil pointer is something, and an interface holding something is not nothing.
© 2026 Preptima. Originally published at preptima.com.
Likely follow-ups
- Rewrite the function so the bug is impossible rather than merely absent.
- Why does a nil map read work while a nil map write panics?
- How would errors.As behave against this value?
- Where else does Go compare a two-word value in a way that surprises people?
Related questions
- Go gives you no project layout and no dependency-injection container. How do you structure a service so it stays testable as it grows?mediumAlso on interfaces and golang5 min
- A promise rejects and nothing is awaiting it. What does Node do?hardAlso on error-handling4 min
- Your logs are full of Cannot set headers after they are sent. Walk me through how a request gets into that state.mediumAlso on error-handling4 min
- Several producers write to one channel and one consumer reads it. Who closes the channel?mediumAlso on golang4 min