main.go
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
package main

import (
	"errors"
	"fmt"
	"log"
)

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

type OpError struct {
	Op  string
	Err error
}

func (e *OpError) Error() string {
	return fmt.Sprintf("%s: %v", e.Op, e.Err)
}

func (e *OpError) Unwrap() error { return e.Err }

func loadUser(id string) error {
	if id == "" {
		return &OpError{Op: "loadUser", Err: errors.New("empty id")}
	}
	if id != "user-123" {
		return &OpError{Op: "loadUser", Err: ErrNotFound}
	}
	return nil
}

func main() {
	err := loadUser("missing")
	if err == nil {
		log.Printf("user loaded")
		return
	}

	if errors.Is(err, ErrNotFound) {
		log.Printf("not found: %v", err)
	}

	var opErr *OpError
	if errors.As(err, &opErr) {
		log.Printf("op=%s root=%v", opErr.Op, opErr.Err)
	}
}

How It Works

Custom error type capturing operation context plus helpers showing errors.Is and errors.As and wrapped stacks.

Defines an OpError struct with operation metadata and an underlying error, implements Unwrap, creates sentinel errors, wraps operations, and demonstrates Is and As on nested errors to classify failures.

Key Concepts

  • 1OpError carries which operation failed for better logs.
  • 2errors.Is and errors.As work because Unwrap exposes the cause.
  • 3Sentinel errors enable callers to branch on error categories.

When to Use This Pattern

  • Library code that needs precise error taxonomy.
  • Mapping errors to user-facing messages or status codes.
  • Testing that specific operations fail as expected.

Best Practices

  • Wrap lower-level errors with enough context to debug quickly.
  • Expose typed errors rather than string-matching messages.
  • Avoid swallowing the cause; always implement Unwrap when wrapping.
Go Version1.13+
Difficultybeginner
Production ReadyYes
Lines of Code47