DEV Community

Cover image for Why Is My nil Error Not nil in Go? Unpacking Nil Interfaces
mahdi
mahdi

Posted on

Why Is My nil Error Not nil in Go? Unpacking Nil Interfaces

If you've written Go for any amount of time, you've probably typed if err != nil hundreds of times. It’s the standard way to handle errors. But sometimes, this check behaves in a way that seems to defy logic, especially when you think you're returning a nil error. This "gotcha" moment almost always comes down to a misunderstanding of how Go's interfaces work, particularly when they are nil.

Let's dive into what a nil interface really is and how to avoid this common pitfall.

What Is an Interface, Really?

First, it's crucial to understand that an interface variable isn't a simple value. It's a container with two parts:

  1. A type: This holds the type of the concrete data stored in the interface (e.g., *bytes.Buffer, *MyError).

  2. A value: This holds the actual value or a pointer to the value (e.g., a pointer to a buffer, nil).

An interface variable is only truly nil when both of these parts are nil. This is the key to the whole mystery.

Let's look at a simple example without even touching errors yet.

package main

import (
    "bytes"
    "fmt"
    "io"
)

func main() {
    // io.Reader is an interface.
    var r io.Reader
    fmt.Println(r == nil) // true

    // bytes.Buffer is a struct
    var b *bytes.Buffer
    fmt.Println(b == nil) // true

    r = b

    // r is now: (type=*bytes.Buffer, value=nil)
    fmt.Println(r == nil) // false
}
Enter fullscreen mode Exit fullscreen mode

When we assign b to r, the interface r gets a type (*bytes.Buffer), so it's no longer considered nil, even though the value it holds is a nil pointer.

The Common error Interface Pitfall

This behavior becomes a real problem when dealing with the error interface. Many people think of error as a built-in primitive type, but it's not. It's an interface defined as:

type error interface {
    Error() string
}
Enter fullscreen mode Exit fullscreen mode

Any type that implements the Error() string method satisfies the error interface. Now, let's see how our new understanding of interfaces can lead to a bug.

Imagine you have a custom error type and a function that might return it.

package main

import "fmt"

type MyError struct {
    Path string
    Msg  string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("%s: %s", e.Path, e.Msg)
}

func processFile(path string) *MyError {

    return nil
}

func main() {

    var err error = processFile("test.txt")


    if err != nil {
        fmt.Printf("Oops, an error occurred: %v\n", err) // This line will run!
        fmt.Printf("Type: %T, Value: %v\n", err, err)
    } else {
        fmt.Println("Everything is fine.")
    }
}
Enter fullscreen mode Exit fullscreen mode

Running this code produces:

Oops, an error occurred: <nil>
Type: *main.MyError, Value: <nil>
Enter fullscreen mode Exit fullscreen mode

The err != nil check passes because the err variable, an error interface, was given a value (nil) that had a concrete type (*MyError). Since the interface's type part was set, the interface itself is no longer nil.

The Solution: Return the Interface

The fix is simple and is a cornerstone of good Go practice: functions should return the error interface type, not a concrete error type.

By changing the function signature, you ensure that when you return nil, you're returning a true nil interface, where both the type and value are nil.

Here’s the corrected code:

package main

import "fmt"

type MyError struct {
    Path string
    Msg  string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("%s: %s", e.Path, e.Msg)
}

func processFileCorrectly(path string) error {
    shouldFail := false 

    if shouldFail {
        return &MyError{Path: path, Msg: "file not found"}
    }


    return nil
}

func main() {
    err := processFileCorrectly("test.txt")

    if err != nil {
        fmt.Printf("Oops, an error occurred: %v\n", err)
    } else {
        fmt.Println("Everything is fine.") // This line runs now!
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaway

A nil pointer to a concrete type is not the same as a nil interface. An interface is only nil if both its internal type and value are nil.

To avoid this trap, always make your functions that can fail return the error type. This way, when you return nil, you're creating a proper "zeroed" interface that will correctly evaluate to true in a == nil check.

Resources

Go Class: 20 Interfaces & Methods in Detail

Interface values with nil underlying values

Nil interface values

Top comments (0)