Skip to Content
Course content

26: Error Wrapping with fmt.Errorf and errors.Is/As

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this happen all the time when developers move from simple scripts to larger Go projects. You start wrapping errors to provide context—which is exactly what you should be doing—but then you realize your error handling logic has mysteriously stopped working. Let's look at a typical example.

package main

import (
	"errors"
	"fmt"
)

var ErrNotFound = errors.New("resource not found")

func findUser(id int) error {
	// Simulate a database miss
	return ErrNotFound
}

func getUserProfile(id int) error {
	err := findUser(id)
	if err != nil {
		// We want to add context so we know where the error happened
		return fmt.Errorf("getUserProfile failed: %v", err)
	}
	return nil
}

func main() {
	err := getUserProfile(123)
	if err == ErrNotFound {
		fmt.Println("Handle 404: User not found")
	} else {
		fmt.Printf("Unexpected error: %v\n", err)
	}
}

The disappearing error identity

If you run the code above, you'll notice it prints Unexpected error: getUserProfile failed: resource not found instead of the 404 handler. Why? Because fmt.Errorf with the %v verb creates a brand new error string. It's a new value entirely. Even though the text looks the same, the original ErrNotFound sentinel is gone, swallowed by the new string. You've effectively "blinded" your calling function to the original cause of the failure.

Preserving the chain with %w

To fix this, Go provides the %w verb. The 'w' stands for wrap. When you use %w, Go doesn't just format a string; it creates a wrapper that maintains a link to the original error. This creates an "error chain" that we can traverse later.

But simply changing %v to %w isn't enough. You also have to stop using the == operator. Because the error is now wrapped, it's no longer equal to the sentinel; it's a wrapper containing the sentinel. This is where errors.Is comes in.

func getUserProfile(id int) error {
	err := findUser(id)
	if err != nil {
		// Notice the %w here instead of %v
		return fmt.Errorf("getUserProfile failed: %w", err)
	}
	return nil
}

func main() {
	err := getUserProfile(123)
	// Use errors.Is to walk the chain and look for ErrNotFound
	if errors.Is(err, ErrNotFound) {
		fmt.Println("Handle 404: User not found")
	}
}

I always tell people to think of errors.Is as "Is this error, or any error in its history, equal to this value?"

Extracting metadata with errors.As

Sometimes you don't just need to know if an error happened, but you need specific data from a custom error type. errors.Is is great for sentinels (simple values), but for structs, you want errors.As.

Imagine your database driver returns a QueryError that contains the specific SQL state code. You can't use errors.Is because every QueryError is a different instance. Instead, errors.As lets you say: "Is there any error in this chain that is of this specific type? If so, please assign it to this variable."

type QueryError struct {
	Query string
	Code  int
}

func (e *QueryError) Error() string {
	return fmt.Sprintf("query %q failed with code %d", e.Query, e.Code)
}

func runQuery() error {
	return fmt.Errorf("db layer: %w", &QueryError{Query: "SELECT...", Code: 5432})
}

func main() {
	err := runQuery()

	var qErr *QueryError
	// If a *QueryError exists anywhere in the chain, it's assigned to qErr
	if errors.As(err, &qErr) {
		fmt.Printf("Database error code: %d\n", qErr.Code)
	}
}

One quick tip: always pass a pointer to the pointer (&qErr) when using errors.As. It feels a bit clunky at first, but it's how the any type internal to the function can actually modify your variable.




📋 Practical Task

Exercise: Implementing a Tiered API Error Handler

You are building a system that fetches weather data. You need to implement a chain of errors that allows a top-level handler to distinguish between a temporary network glitch and a permanent configuration error, even after the errors have been wrapped with context.

Requirements:

  • Create two sentinel errors: ErrNetworkTimeout and ErrInvalidAPIKey.
  • Create a custom struct APIError that includes a field StatusCode int and implements the Error() method.
  • Write a function fetchWeather() error that returns a QueryError wrapped inside a generic error using fmt.Errorf and %w.
  • Write a function validateKey() error that returns ErrInvalidAPIKey wrapped inside another error.
  • In main(), call these functions and use errors.Is to detect the ErrInvalidAPIKey and errors.As to extract the StatusCode from the APIError.

Your final code should demonstrate that you can identify the root cause of the error regardless of how many layers of context have been added by fmt.Errorf.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.