Skip to Content
Course content

149: errors.Is and errors.As

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

Imagine you're sorting through a stack of returned mail. Some envelopes are plain, but some are wrapped in three layers of protective packaging—bubble wrap, a cardboard box, and then a plastic mailer. If you're looking for a specific "Return to Sender" stamp from the post office, you don't just look at the outermost plastic mailer and give up if you don't see the stamp. You peel back the layers until you find the original envelope inside. If that stamp is anywhere in the chain, the package is a return.

That's exactly how errors.Is and errors.As work in Go. Since Go 1.13, we've been "wrapping" errors to add context (using fmt.Errorf("...: %w", err)). Because of this, a simple if err == sql.ErrNoRows often fails because the error isn't sql.ErrNoRows anymore—it's a wrapper containing sql.ErrNoRows.

Stop comparing errors with the double-equals

I've seen countless bugs where developers use == to check for a sentinel error, only to find that their error handling logic stopped working the moment they added a bit of context to their logs. When you wrap an error, you're creating a new error value. The original is still there, but it's hidden inside.

errors.Is is your way of peeling back those layers. It recursively unwraps the error chain until it finds a match or hits the end. Here is how you should be doing it when dealing with sentinel errors (those predefined error variables):

package main

import (
	"errors"
	"fmt"
	"os"
)

var ErrDiskFull = errors.New("disk is full")

func saveFile() error {
	// We wrap the sentinel error to provide more context
	return fmt.Errorf("failed to save user profile: %w", ErrDiskFull)
}

func main() {
	err := saveFile()

	// This would be FALSE because err is a wrapped error, not ErrDiskFull itself
	if err == ErrDiskFull {
		fmt.Println("Direct match found!")
	}

	// This is TRUE because it searches the entire wrap chain
	if errors.Is(err, ErrDiskFull) {
		fmt.Println("Found the disk full error deep in the chain!")
	}
}

Extracting data with errors.As

Now, sometimes you don't just want to know if an error happened; you need to get your hands on the data inside the error. Maybe you have a custom error struct that contains an HTTP status code or a field name that failed validation. Using a type assertion (like err.(*MyError)) is risky because, again, the error might be wrapped.

errors.As is like saying: "Search through this chain of errors. If you find one that matches this specific type, assign it to my variable and let me know."

package main

import (
	"errors"
	"fmt"
)

type QueryError struct {
	Query string
	Err   error
}

func (e *QueryError) Error() string {
	return fmt.Sprintf("query %q failed: %v", e.Query, e.Err)
}

func runQuery() error {
	qErr := &QueryError{Query: "SELECT * FROM users", Err: errors.New("timeout")}
	return fmt.Errorf("database layer: %w", qErr)
}

func main() {
	err := runQuery()

	var target *QueryError
	// This searches the chain for anything that can be assigned to *QueryError
	if errors.As(err, &target) {
		fmt.Printf("The failing query was: %s\n", target.Query)
	}
}

Notice that I passed &target (a pointer to a pointer) into errors.As. This is a common point of confusion. The function needs to be able to modify your target variable to point to the actual error found in the chain, which is why it requires a pointer to that variable.

When to use which?

It's a simple rule of thumb I use: Use errors.Is for "Which one is this?" (comparing values). Use errors.As for "What is this?" (checking types and extracting data). If you find yourself writing if err != nil followed by a type assertion, you should probably be using errors.As instead.




📋 Practical Task

Implementing a Robust API Rate Limit Handler

You are building a middleware for an API. You need to handle two types of failures: a generic "Service Unavailable" sentinel error and a specific "RateLimitError" struct that contains the time the user can try again.

Requirements:

  • Create a sentinel error named ErrServiceUnavailable.
  • Create a struct RateLimitError with a field RetryAfter int (representing seconds). Ensure it implements the error interface.
  • Write a function SimulateRequest() error that randomly returns one of the two errors, but wraps them using fmt.Errorf and the %w verb to simulate a call stack.
  • In main, call SimulateRequest() and use errors.Is to detect the service unavailability and errors.As to extract the RetryAfter value from the rate limit error.
  • Print a specific message for each case: "Service is down, try later" or "Rate limited! Please wait X seconds."
Rating
0 0

There are no comments for now.

to be the first to leave a comment.