DEV Community

Cover image for The Secret Life of Go: Error Handling
Aaron Rose
Aaron Rose

Posted on

The Secret Life of Go: Error Handling

Sentinel Errors, Wrapping, and The String Trap


Eleanor is a senior software engineer. Ethan is her junior colleague. They work in a beautiful beaux arts library in Lower Manhattan β€” the kind of place where coding languages are discussed like poetry.

Episode 30

Ethan was reviewing an HTTP handler he had just written. He wanted to return a 404 Not Found if a database query failed, and a 500 Internal Server Error for anything else.

"How does this look?" he asked, pointing to his screen.

user, err := db.GetUser(id)
if err != nil {
    // If the error message contains the words "not found", it's a 404
    if strings.Contains(err.Error(), "not found") {
        return respondWithError(w, 404, "User not found")
    }

    // Otherwise, it's a real server error
    return respondWithError(w, 500, "Internal server error")
}
Enter fullscreen mode Exit fullscreen mode

Eleanor leaned in, her eyes scanning the strings.Contains line. "That," she said softly, "is a ticking time bomb."

"Why?" Ethan asked. "It works perfectly in my tests."

"It works today," Eleanor corrected. "But what happens next month when we update our database driver, and the library authors change their error message from 'record not found' to 'no rows in result set'?"

Ethan blinked. "My strings.Contains would fail. The app would start returning 500s instead of 404s, and pager alerts would go off at 3 AM."

"Exactly. Checking an error's string value is incredibly brittle," Eleanor explained. "In Go, an error isn't just a string for a human to read. It is a piece of state for your program to inspect."

The Sentinel Error

Eleanor opened the database package file. "Instead of relying on random text, a package should declare its specific failure states as exported variables. We call these Sentinel Errorsβ€”they stand guard, representing a specific, known failure."

She typed at the top of the file:

import "errors"

// ErrNotFound is a Sentinel Error. 
// It is exported (capitalized) so other packages can check against it.
var ErrNotFound = errors.New("record not found")
Enter fullscreen mode Exit fullscreen mode

"Now," she said, switching back to the HTTP handler, "we check against the Sentinel using errors.Is."

user, err := db.GetUser(id)
if err != nil {
    // We ask Go: "Is this specific error inside the chain?"
    if errors.Is(err, database.ErrNotFound) {
        return respondWithError(w, 404, "User not found")
    }
    return respondWithError(w, 500, "Internal server error")
}
Enter fullscreen mode Exit fullscreen mode

Ethan nodded. "That's much safer. If they change the text inside ErrNotFound later, my if statement still works because it's checking the memory address of the variable, not the string."

The Wrapping Problem (%v vs %w)

Ethan frowned at the screen. "But wait. If the database layer just returns ErrNotFound, the HTTP handler won't know which user ID failed. I need to add context to the error before I return it up the stack."

He quickly typed out a solution:

func GetUser(id int) (User, error) {
    // ... db lookup fails ...

    // Add context to the Sentinel error using fmt.Errorf and %v (value)
    return User{}, fmt.Errorf("failed to fetch user %d: %v", id, ErrNotFound)
}
Enter fullscreen mode Exit fullscreen mode

"Don't do that!" Eleanor warned, catching his hand before he hit save.

"If you use %v (value) or %s (string), fmt.Errorf creates a brand new, flattened string. It destroys the original Sentinel Error. When your HTTP handler calls errors.Is(err, ErrNotFound), it will return false. The Sentinel is gone."

"So how do I add context without destroying the Sentinel?"

"You wrap it," Eleanor said, smiling. "Go 1.13 gave us a superpower. We use the %w verb."

She changed one character in his code:

func GetUser(id int) (User, error) {
    // ... db lookup fails ...

    // Use %w to WRAP the error
    return User{}, fmt.Errorf("failed to fetch user %d: %w", id, ErrNotFound)
}
Enter fullscreen mode Exit fullscreen mode

"Think of %w like a Matryoshka doll," Eleanor explained. "It creates a new error with your helpful context ('failed to fetch user 42'), but it hides the original ErrNotFound securely inside it. It creates a linked list of errors."

"And errors.Is knows how to open the dolls?" Ethan asked.

"Exactly," Eleanor said. "When you call errors.Is(err, ErrNotFound), Go automatically unwraps the dolls, layer by layer, checking each one to see if it matches the Sentinel you are looking for."

Ethan looked at his refactored code. It was clean, type-safe, and completely immune to string-formatting bugs.

"I was treating errors like console logs," Ethan realized. "I was just formatting text."

"It's the most common mistake in Go," Eleanor agreed. "An error is an API. You are designing a programmable interface of failure states. Build it so the machine can read it, and the humans will be fine."


Key Concepts

The String Trap

  • Never use strings.Contains(err.Error(), "...") to determine your application's control flow. Error strings are meant for humans and logging; they are subject to change and will break your logic silently.

Sentinel Errors

  • A package-level, exported error variable (e.g., var ErrNotFound = errors.New("...")) used to indicate a specific, expected failure state.
  • Callers can use these variables to make decisions without relying on strings.

Error Wrapping (%w)

  • When returning an error up the stack, you often want to add context (e.g., "failed to open config file").
  • Using fmt.Errorf("... %v", err) destroys the original error type.
  • Using fmt.Errorf("... %w", err) wraps the error, creating a chain. The context is added, but the original error is preserved inside.

errors.Is

  • Replaces the old if err == ErrNotFound pattern.
  • errors.Is automatically unwraps the error chain (the Russian nesting dolls) and checks if the target Sentinel error exists anywhere in the chain.

Aaron Rose is a software engineer and technology writer at tech-reader.blog. For explainer videos and podcasts, check out Tech-Reader YouTube channel.

Top comments (0)