SyntaxStudy
Sign Up
Go Error Wrapping, Unwrapping, and Sentinel Errors
Go Beginner 1 min read

Error Wrapping, Unwrapping, and Sentinel Errors

Go 1.13 introduced the `%w` verb for `fmt.Errorf`, which wraps an error so that the original error can be retrieved using `errors.Unwrap`. The `errors.Is` function checks whether any error in the chain matches a target value (using `==` comparison), and `errors.As` checks whether any error in the chain can be assigned to a target type. These functions traverse the entire error chain. Sentinel errors are package-level variables declared with `errors.New` that represent well-known error conditions. Examples from the standard library include `io.EOF`, `os.ErrNotExist`, and `sql.ErrNoRows`. Callers check for sentinel errors using `errors.Is(err, io.EOF)`, which correctly handles wrapped errors unlike direct comparison with `==`. When designing error types, consider whether callers need to make decisions based on the error. If callers only need to know that something failed and log a message, a simple error string is sufficient. If callers need to handle specific conditions differently (retry on timeout, display a user-friendly message for validation errors), define a custom error type or sentinel error.
Example
package main

import (
    "errors"
    "fmt"
    "io/fs"
    "os"
)

// Sentinel errors
var (
    ErrNotFound    = errors.New("not found")
    ErrUnauthorised = errors.New("unauthorised")
)

type DBError struct {
    Code    int
    Message string
}

func (e *DBError) Error() string {
    return fmt.Sprintf("db error %d: %s", e.Code, e.Message)
}

func queryUser(id int) error {
    if id <= 0 {
        return fmt.Errorf("queryUser: %w", ErrNotFound)
    }
    if id == 999 {
        return fmt.Errorf("queryUser: %w", &DBError{Code: 500, Message: "connection lost"})
    }
    return nil
}

func main() {
    // errors.Is — checks the chain
    err := queryUser(0)
    fmt.Println(errors.Is(err, ErrNotFound)) // true

    // errors.As — extracts a specific type from the chain
    err = queryUser(999)
    var dbErr *DBError
    if errors.As(err, &dbErr) {
        fmt.Printf("DB code=%d msg=%s\n", dbErr.Code, dbErr.Message)
    }

    // Standard library sentinel error
    _, err = os.Open("/nonexistent/file.txt")
    if errors.Is(err, fs.ErrNotExist) {
        fmt.Println("file does not exist (sentinel matched)")
    }

    // Wrapping chain
    base := errors.New("base error")
    w1   := fmt.Errorf("layer 1: %w", base)
    w2   := fmt.Errorf("layer 2: %w", w1)
    fmt.Println("Is base:", errors.Is(w2, base)) // true through chain
    fmt.Println("chain:", w2)
}