Error Wrapping & Best Practices
Error wrapping adds context while preserving the original error. This creates an error chain that helps with debugging and allows checking for specific errors. In this lesson, you'll master error wrapping and learn professional error handling practices.
Error Wrapping with %w
The %w verb in fmt.Errorf wraps an error:
Click Run to execute your code
%w- Wraps the error (preserves it for errors.Is/As)%v- Converts to string (loses the original error)
// Wrapping (preserves original)
err := fmt.Errorf("failed to process: %w", originalErr)
// Not wrapping (just formatting)
err := fmt.Errorf("failed to process: %v", originalErr)
%w when you want to preserve
the error for
checking with errors.Is() or errors.As(). Use %v when you want to
hide
implementation details.
Error Chains
Wrapping creates a chain of errors:
Click Run to execute your code
Unwrapping Errors
import "errors"
// Unwrap returns the wrapped error
err := fmt.Errorf("outer: %w", innerErr)
unwrapped := errors.Unwrap(err) // Returns innerErr
// Unwrap returns nil if not wrapped
simple := errors.New("simple error")
unwrapped := errors.Unwrap(simple) // nil
Working with Wrapped Errors
errors.Is() - Check Error Type
var ErrNotFound = errors.New("not found")
func findUser(id int) error {
// ... some code ...
return fmt.Errorf("user lookup failed: %w", ErrNotFound)
}
err := findUser(123)
// errors.Is() works through the chain
if errors.Is(err, ErrNotFound) {
fmt.Println("User not found!") // This works!
}
// Direct comparison doesn't work
if err == ErrNotFound {
fmt.Println("Won't print") // err is wrapped
}
errors.As() - Extract Error Type
type ValidationError struct {
Field string
Value interface{}
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("invalid %s: %v", e.Field, e.Value)
}
func validate(age int) error {
if age < 0 {
return fmt.Errorf("validation failed: %w",
&ValidationError{Field: "age", Value: age})
}
return nil
}
err := validate(-5)
// errors.As() extracts the ValidationError
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf("Field: %s, Value: %v\n", ve.Field, ve.Value)
}
Error Handling Best Practices
1. Add Context at Each Layer
// Database layer
func (db *DB) GetUser(id int) (*User, error) {
user, err := db.query(id)
if err != nil {
return nil, fmt.Errorf("database query failed: %w", err)
}
return user, nil
}
// Service layer
func (s *Service) LoadUser(id int) (*User, error) {
user, err := s.db.GetUser(id)
if err != nil {
return nil, fmt.Errorf("failed to load user %d: %w", id, err)
}
return user, nil
}
// Handler layer
func (h *Handler) HandleRequest(id int) error {
user, err := h.service.LoadUser(id)
if err != nil {
return fmt.Errorf("request handling failed: %w", err)
}
// ...
}
2. Don't Wrap Errors Unnecessarily
// β Too much wrapping
func process() error {
err := doSomething()
if err != nil {
return fmt.Errorf("error: %w", err) // Not adding value
}
return nil
}
// β
Add meaningful context
func process() error {
err := doSomething()
if err != nil {
return fmt.Errorf("failed to process data: %w", err)
}
return nil
}
3. Handle Errors at the Right Level
// β Wrong - handling too early
func readConfig() (*Config, error) {
data, err := os.ReadFile("config.json")
if err != nil {
log.Fatal(err) // Don't log.Fatal in library code!
}
// ...
}
// β
Correct - return error, let caller decide
func readConfig() (*Config, error) {
data, err := os.ReadFile("config.json")
if err != nil {
return nil, fmt.Errorf("failed to read config: %w", err)
}
// ...
}
// Caller decides how to handle
func main() {
config, err := readConfig()
if err != nil {
log.Fatal(err) // OK in main
}
}
4. Use Sentinel Errors for Expected Conditions
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidInput = errors.New("invalid input")
)
func getUser(id int) (*User, error) {
if id <= 0 {
return nil, ErrInvalidInput
}
user := findInDB(id)
if user == nil {
return nil, ErrNotFound
}
return user, nil
}
// Caller can check specific errors
user, err := getUser(123)
if errors.Is(err, ErrNotFound) {
// Handle not found
} else if errors.Is(err, ErrInvalidInput) {
// Handle invalid input
}
Logging Errors
Log Once, Return Once
// β Wrong - logging at every level
func layer1() error {
err := layer2()
if err != nil {
log.Printf("layer1 error: %v", err) // Logged
return err
}
return nil
}
func layer2() error {
err := layer3()
if err != nil {
log.Printf("layer2 error: %v", err) // Logged again!
return err
}
return nil
}
// β
Correct - log at top level only
func layer1() error {
err := layer2()
if err != nil {
return fmt.Errorf("layer1: %w", err) // Add context, don't log
}
return nil
}
func main() {
err := layer1()
if err != nil {
log.Printf("Error: %v", err) // Log once at top
}
}
Common Mistakes
1. Using %v instead of %w
// β Wrong - loses original error
err := fmt.Errorf("failed: %v", originalErr)
if errors.Is(err, ErrNotFound) { // Won't work!
// ...
}
// β
Correct - preserves original
err := fmt.Errorf("failed: %w", originalErr)
if errors.Is(err, ErrNotFound) { // Works!
// ...
}
2. Wrapping nil errors
// β Wrong - wrapping nil
func process() error {
err := doSomething()
return fmt.Errorf("process failed: %w", err) // err might be nil!
}
// β
Correct - check before wrapping
func process() error {
err := doSomething()
if err != nil {
return fmt.Errorf("process failed: %w", err)
}
return nil
}
3. Exposing internal errors
// β Wrong - exposing database details to API
func (api *API) GetUser(id int) (*User, error) {
user, err := db.Query("SELECT * FROM users WHERE id = ?", id)
return user, err // Exposes SQL errors!
}
// β
Correct - wrap with generic message
func (api *API) GetUser(id int) (*User, error) {
user, err := db.Query("SELECT * FROM users WHERE id = ?", id)
if err != nil {
return nil, fmt.Errorf("failed to get user: %w", ErrInternal)
}
return user, nil
}
Exercise: File Processor with Error Handling
Task: Create a file processor with proper error handling.
Requirements:
- Create functions: readFile, processData, writeFile
- Wrap errors with context at each layer
- Use sentinel errors for specific conditions
- Handle errors appropriately in main
Show Solution
package main
import (
"errors"
"fmt"
"os"
"strings"
)
// Sentinel errors
var (
ErrEmptyFile = errors.New("file is empty")
ErrInvalidData = errors.New("invalid data format")
)
// readFile reads a file
func readFile(filename string) (string, error) {
data, err := os.ReadFile(filename)
if err != nil {
return "", fmt.Errorf("failed to read file %s: %w", filename, err)
}
if len(data) == 0 {
return "", fmt.Errorf("file %s: %w", filename, ErrEmptyFile)
}
return string(data), nil
}
// processData processes the data
func processData(data string) (string, error) {
if !strings.Contains(data, ":") {
return "", fmt.Errorf("data processing: %w", ErrInvalidData)
}
// Simple processing: convert to uppercase
processed := strings.ToUpper(data)
return processed, nil
}
// writeFile writes data to a file
func writeFile(filename, data string) error {
err := os.WriteFile(filename, []byte(data), 0644)
if err != nil {
return fmt.Errorf("failed to write file %s: %w", filename, err)
}
return nil
}
// processFile orchestrates the entire process
func processFile(inputFile, outputFile string) error {
// Read
data, err := readFile(inputFile)
if err != nil {
return fmt.Errorf("processFile: %w", err)
}
// Process
processed, err := processData(data)
if err != nil {
return fmt.Errorf("processFile: %w", err)
}
// Write
err = writeFile(outputFile, processed)
if err != nil {
return fmt.Errorf("processFile: %w", err)
}
return nil
}
func main() {
err := processFile("input.txt", "output.txt")
if err != nil {
// Check for specific errors
if errors.Is(err, os.ErrNotExist) {
fmt.Println("Error: Input file does not exist")
} else if errors.Is(err, ErrEmptyFile) {
fmt.Println("Error: Input file is empty")
} else if errors.Is(err, ErrInvalidData) {
fmt.Println("Error: Data format is invalid")
} else {
fmt.Printf("Error: %v\n", err)
}
os.Exit(1)
}
fmt.Println("File processed successfully!")
}
Summary
- %w verb wraps errors, preserving them for Is/As
- %v verb formats errors as strings (doesn't wrap)
- Error chains provide context at each layer
- errors.Is() checks wrapped errors
- errors.As() extracts custom error types
- errors.Unwrap() returns the wrapped error
- Add context at each layer, but don't over-wrap
- Log once at the top level, return errors below
What's Next?
Congratulations on completing the Error Handling module! You now understand Go's error philosophy and best practices. Next, you'll dive into Concurrencyβone of Go's most powerful features with goroutines and channels!
Enjoying these tutorials?