Menu
Coddy logo textTech

errors.Is() and errors.As()

Part of the Object Oriented Programming section of Coddy's GO journey — lesson 56 of 107.

When errors get wrapped, simple equality checks like err == ErrNotFound no longer work. The errors package provides two functions to inspect wrapped error chains: errors.Is() and errors.As().

errors.Is() checks if any error in the chain matches a specific sentinel error:

import "errors"

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

func FindUser(id int) error {
    return fmt.Errorf("database lookup failed: %w", ErrNotFound)
}

err := FindUser(42)
if errors.Is(err, ErrNotFound) {
    fmt.Println("User doesn't exist")
}

Even though the returned error is wrapped, errors.Is() unwraps the chain and finds ErrNotFound inside.

errors.As() checks if any error in the chain matches a specific type and extracts it:

type ValidationError struct {
    Field string
}

func (e *ValidationError) Error() string {
    return "validation failed: " + e.Field
}

err := fmt.Errorf("request failed: %w", &ValidationError{Field: "email"})

var ve *ValidationError
if errors.As(err, &ve) {
    fmt.Println("Invalid field:", ve.Field)
}

Use errors.Is() when checking for sentinel errors, and errors.As() when you need to access fields from a custom error type. Both functions traverse the entire error chain, making them essential for handling wrapped errors properly.

challenge icon

Challenge

Easy

Let's build a file processing system that demonstrates how to properly inspect wrapped error chains using errors.Is() and errors.As(). You'll create errors that get wrapped as they pass through different layers, then use these functions to identify and extract information from them.

You'll organize your code across two files:

  • errors.go: Define the error infrastructure for your file system.

    Create a sentinel error:

    • ErrPermissionDenied with message "permission denied"

    Create a custom error type:

    • FileError struct with Filename (string) and Operation (string) fields
    • Its Error() method should return: [Operation] failed on file: [Filename]

    Implement two functions that simulate file operations and wrap errors:

    • ReadFile(filename string) error - If filename is "secret.txt", return ErrPermissionDenied. If filename is "missing.txt", return a *FileError with Operation "read" and the given Filename. Otherwise return nil.
    • ProcessFile(filename string) error - Calls ReadFile. If an error is returned, wrap it using fmt.Errorf with the format "processing failed: %w". Otherwise return nil.
  • main.go: Read a filename from input and call ProcessFile. Use errors.Is() and errors.As() to inspect the wrapped error chain and print appropriate messages based on what you find inside.

The following input will be provided:

  • Line 1: Filename to process

Handle the result by inspecting the error chain:

  • If errors.Is() finds ErrPermissionDenied in the chain: print the full error message, then on a new line print Access denied - check file permissions
  • If errors.As() finds a *FileError in the chain: print the full error message, then on a new line print File issue: [Filename] during [Operation]
  • If no error occurs: print File '[filename]' processed successfully

For example, given secret.txt, your output should be:

processing failed: permission denied
Access denied - check file permissions

And given missing.txt, your output should be:

processing failed: read failed on file: missing.txt
File issue: missing.txt during read

And given data.txt, your output should be:

File 'data.txt' processed successfully

Notice how even though the errors are wrapped by ProcessFile, you can still detect the original sentinel error with errors.Is() and extract the custom error type with errors.As(). These functions traverse the entire error chain to find what you're looking for.

Cheat sheet

When errors are wrapped, use errors.Is() and errors.As() from the errors package to inspect the error chain.

errors.Is() checks if any error in the chain matches a sentinel error:

import "errors"

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

err := fmt.Errorf("database lookup failed: %w", ErrNotFound)
if errors.Is(err, ErrNotFound) {
    // ErrNotFound was found in the chain
}

errors.As() checks if any error in the chain matches a specific type and extracts it:

type ValidationError struct {
    Field string
}

func (e *ValidationError) Error() string {
    return "validation failed: " + e.Field
}

err := fmt.Errorf("request failed: %w", &ValidationError{Field: "email"})

var ve *ValidationError
if errors.As(err, &ve) {
    // ve now contains the ValidationError from the chain
    fmt.Println(ve.Field)
}

Use errors.Is() for sentinel errors and errors.As() when you need to access fields from custom error types. Both functions traverse the entire error chain.

Try it yourself

package main

import (
	"errors"
	"fmt"
)

func main() {
	// Read the filename from input
	var filename string
	fmt.Scanln(&filename)

	// Call ProcessFile with the filename
	err := ProcessFile(filename)

	// TODO: Handle the result by inspecting the error chain
	// 
	// Use errors.Is() to check if ErrPermissionDenied is in the chain:
	// - If found: print the full error, then "Access denied - check file permissions"
	//
	// Use errors.As() to check if a *FileError is in the chain:
	// - If found: print the full error, then "File issue: [Filename] during [Operation]"
	//
	// If no error: print "File '[filename]' processed successfully"

	_ = errors.Is // hint: use errors.Is()
	_ = errors.As // hint: use errors.As()
	_ = err
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming