Error Wrapping and Unwrapping
Since Go 1.13, error wrapping has been built into the standard library. Using %w with fmt.Errorf and errors.Is/errors.As, you can trace the root cause through an error chain.
Wrapping Errors with %wβ
Using %w in fmt.Errorf wraps an existing error while adding context information.
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")
func readFromDB(id int) (string, error) {
if id <= 0 {
return "", ErrNotFound
}
return fmt.Sprintf("data-%d", id), nil
}
func getUser(id int) (string, error) {
data, err := readFromDB(id)
if err != nil {
// Wrap with %w β add context
return "", fmt.Errorf("getUser(id=%d): %w", id, err)
}
return data, nil
}
func handleRequest(id int) error {
_, err := getUser(id)
if err != nil {
// Wrap once more
return fmt.Errorf("handleRequest: %w", err)
}
return nil
}
func main() {
err := handleRequest(-1)
if err != nil {
// Full error message (including chain)
fmt.Println("error:", err)
// error: handleRequest: getUser(id=-1): not found
// Check original error β errors.Is searches the entire chain
fmt.Println("ErrNotFound?", errors.Is(err, ErrNotFound)) // true
// Unwrap one level at a time with errors.Unwrap
fmt.Println("one unwrap:", errors.Unwrap(err))
// one unwrap: getUser(id=-1): not found
fmt.Println("two unwraps:", errors.Unwrap(errors.Unwrap(err)))
// two unwraps: not found
}
}
errors.Is β Searching the Error Chainβ
errors.Is traverses the error chain to check whether it matches the target error.
package main
import (
"errors"
"fmt"
)
// Sentinel error definitions
var (
ErrNotFound = errors.New("not found")
ErrPermission = errors.New("permission denied")
ErrTimeout = errors.New("timeout")
)
// Building an error chain
func level3() error {
return ErrNotFound
}
func level2() error {
err := level3()
return fmt.Errorf("level2 failed: %w", err)
}
func level1() error {
err := level2()
return fmt.Errorf("level1 failed: %w", err)
}
// Custom error with Is method implemented (advanced)
type TimeoutError struct {
Code int
}
func (e *TimeoutError) Error() string {
return fmt.Sprintf("timeout (code=%d)", e.Code)
}
// Is method: compare whether it is the same type of error
func (e *TimeoutError) Is(target error) bool {
t, ok := target.(*TimeoutError)
if !ok {
return false
}
// Code 0 matches any TimeoutError (wildcard)
return t.Code == 0 || e.Code == t.Code
}
func main() {
// Basic errors.Is
err := level1()
fmt.Println("error:", err)
// error: level1 failed: level2 failed: not found
fmt.Println("Is ErrNotFound:", errors.Is(err, ErrNotFound)) // true
fmt.Println("Is ErrPermission:", errors.Is(err, ErrPermission)) // false
fmt.Println("Is ErrTimeout:", errors.Is(err, ErrTimeout)) // false
// Direct comparison β does not search the chain
fmt.Println("\ndirect compare:", err == ErrNotFound) // false! it's wrapped
// Is method implementation example
specificErr := &TimeoutError{Code: 503}
wrappedErr := fmt.Errorf("request failed: %w", specificErr)
// Check for code 503
fmt.Println("\n503 timeout?", errors.Is(wrappedErr, &TimeoutError{Code: 503})) // true
// Check any TimeoutError with code 0
fmt.Println("any timeout?", errors.Is(wrappedErr, &TimeoutError{Code: 0})) // true
// Not code 404
fmt.Println("404 timeout?", errors.Is(wrappedErr, &TimeoutError{Code: 404})) // false
// Practical: HTTP error handling pattern
var ErrHTTP404 = errors.New("HTTP 404")
var ErrHTTP500 = errors.New("HTTP 500")
handleError := func(err error) {
switch {
case errors.Is(err, ErrHTTP404):
fmt.Println("resource not found")
case errors.Is(err, ErrHTTP500):
fmt.Println("internal server error")
case errors.Is(err, ErrNotFound):
fmt.Println("data not found")
default:
fmt.Println("unknown error:", err)
}
}
handleError(fmt.Errorf("api: %w", ErrHTTP404))
handleError(fmt.Errorf("db: %w", ErrNotFound))
}
errors.As β Extracting Error Typesβ
errors.As finds and extracts a specific type of error from the error chain.
package main
import (
"errors"
"fmt"
)
// Custom error types with detailed information
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: field=%q msg=%q", e.Field, e.Message)
}
type DatabaseError struct {
Query string
Code int
}
func (e *DatabaseError) Error() string {
return fmt.Sprintf("database error: query=%q code=%d", e.Query, e.Code)
}
// Functions that produce errors
func validateAge(age int) error {
if age < 0 || age > 150 {
return &ValidationError{Field: "age", Message: "must be between 0 and 150"}
}
return nil
}
func queryUser(id int) (string, error) {
if id == 0 {
return "", &DatabaseError{Query: "SELECT * FROM users WHERE id=0", Code: 1048}
}
return fmt.Sprintf("user-%d", id), nil
}
func processUser(id, age int) error {
if err := validateAge(age); err != nil {
return fmt.Errorf("processUser: %w", err)
}
if _, err := queryUser(id); err != nil {
return fmt.Errorf("processUser: %w", err)
}
return nil
}
func main() {
// errors.As β type-specific error handling
err := processUser(1, -5) // invalid age
if err != nil {
fmt.Println("error:", err)
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("validation failed: field=%q, message=%q\n",
valErr.Field, valErr.Message)
// validation failed: field="age", message="must be between 0 and 150"
}
}
// DB error
err = processUser(0, 25) // invalid ID
if err != nil {
fmt.Println("error:", err)
var dbErr *DatabaseError
if errors.As(err, &dbErr) {
fmt.Printf("DB error: query=%q, code=%d\n", dbErr.Query, dbErr.Code)
// DB error: query="SELECT * FROM users WHERE id=0", code=1048
}
}
// errors.Is vs errors.As comparison
fmt.Println("\n--- Is vs As comparison ---")
wrappedErr := fmt.Errorf("outer: %w", &ValidationError{Field: "email", Message: "invalid format"})
// errors.Is: identity comparison (values must match)
fmt.Println("Is ValidationError{} ?:", errors.Is(wrappedErr, &ValidationError{})) // false (different values)
// errors.As: type compatibility check then extraction
var extracted *ValidationError
fmt.Println("As *ValidationError ?:", errors.As(wrappedErr, &extracted)) // true
if extracted != nil {
fmt.Printf("extracted error: field=%q\n", extracted.Field) // extracted error: field="email"
}
}
Full Error Chain Traversalβ
Pattern for directly iterating through an error chain.
package main
import (
"errors"
"fmt"
)
// Build a nested wrapped error chain
func buildErrorChain() error {
base := errors.New("base error")
wrapped1 := fmt.Errorf("layer 1: %w", base)
wrapped2 := fmt.Errorf("layer 2: %w", wrapped1)
wrapped3 := fmt.Errorf("layer 3: %w", wrapped2)
return wrapped3
}
// Collect error chain into a slice
func collectErrors(err error) []error {
var chain []error
for err != nil {
chain = append(chain, err)
err = errors.Unwrap(err)
}
return chain
}
// Join multiple errors (Go 1.20+)
func processAll(ids []int) error {
var errs []error
for _, id := range ids {
if id <= 0 {
errs = append(errs, fmt.Errorf("invalid id: %d", id))
}
}
if len(errs) > 0 {
return errors.Join(errs...) // Go 1.20+
}
return nil
}
func main() {
// Manual chain traversal
err := buildErrorChain()
chain := collectErrors(err)
fmt.Printf("error chain depth: %d\n", len(chain))
for i, e := range chain {
fmt.Printf(" [%d] %v\n", i, e)
}
// error chain depth: 4
// [0] layer 3: layer 2: layer 1: base error
// [1] layer 2: layer 1: base error
// [2] layer 1: base error
// [3] base error
// errors.Join usage (Go 1.20+)
fmt.Println()
joinErr := processAll([]int{1, -1, 0, 2, -3})
if joinErr != nil {
fmt.Println("compound error:", joinErr)
// Joined errors implement an interface where Unwrap() returns []error
type unwrapper interface {
Unwrap() []error
}
if uw, ok := joinErr.(unwrapper); ok {
errs := uw.Unwrap()
fmt.Printf("total %d error(s):\n", len(errs))
for _, e := range errs {
fmt.Printf(" - %v\n", e)
}
}
}
// Practical: extracting root cause from error chain
rootCause := func(err error) error {
for {
unwrapped := errors.Unwrap(err)
if unwrapped == nil {
return err
}
err = unwrapped
}
}
chainErr := buildErrorChain()
fmt.Println("\nroot cause:", rootCause(chainErr)) // root cause: base error
}
Practical Example: Layered Error Handlingβ
Using error wrapping in a real application pattern.
package main
import (
"errors"
"fmt"
)
// Domain errors
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidInput = errors.New("invalid input")
ErrUnauthorized = errors.New("unauthorized")
)
// Each layer adds error context
type UserRepository struct{}
func (r *UserRepository) FindByID(id int) (map[string]any, error) {
if id <= 0 {
return nil, fmt.Errorf("repository.FindByID: %w", ErrInvalidInput)
}
if id > 100 {
return nil, fmt.Errorf("repository.FindByID(id=%d): %w", id, ErrUserNotFound)
}
return map[string]any{"id": id, "name": fmt.Sprintf("User%d", id), "role": "user"}, nil
}
type UserService struct {
repo *UserRepository
}
func (s *UserService) GetProfile(requesterID, targetID int) (map[string]any, error) {
if requesterID <= 0 {
return nil, fmt.Errorf("service.GetProfile: requester: %w", ErrUnauthorized)
}
user, err := s.repo.FindByID(targetID)
if err != nil {
return nil, fmt.Errorf("service.GetProfile(target=%d): %w", targetID, err)
}
return user, nil
}
type UserHandler struct {
svc *UserService
}
func (h *UserHandler) Handle(requesterID, targetID int) {
profile, err := h.svc.GetProfile(requesterID, targetID)
if err != nil {
fmt.Printf("handler error: %v\n", err)
// Determine HTTP status code based on error type
switch {
case errors.Is(err, ErrUnauthorized):
fmt.Println("β HTTP 401 Unauthorized")
case errors.Is(err, ErrUserNotFound):
fmt.Println("β HTTP 404 Not Found")
case errors.Is(err, ErrInvalidInput):
fmt.Println("β HTTP 400 Bad Request")
default:
fmt.Println("β HTTP 500 Internal Server Error")
}
return
}
fmt.Printf("profile fetched successfully: %v\n", profile)
}
func main() {
repo := &UserRepository{}
svc := &UserService{repo: repo}
handler := &UserHandler{svc: svc}
fmt.Println("=== Normal fetch ===")
handler.Handle(1, 42)
fmt.Println("\n=== Auth failure ===")
handler.Handle(0, 42)
fmt.Println("\n=== User not found ===")
handler.Handle(1, 999)
fmt.Println("\n=== Invalid input ===")
handler.Handle(1, -1)
}
Key Summary
fmt.Errorf("msg: %w", err)β wraps an error while adding contexterrors.Is(err, target)β searches the entire chain for a specific error valueerrors.As(err, &target)β extracts a specific type of error from the chainerrors.Unwrap(err)β unwrap one levelerrors.Join(errs...)β combine multiple errors into one (Go 1.20+)- Error wrapping adds context while preserving the root cause