Go > Error Handling > Built-in Error Interface > Wrapping errors

Wrapping Errors in Go

This example demonstrates how to wrap errors in Go, providing context and preserving the original error's information. Error wrapping is crucial for creating robust and maintainable applications.

Understanding Error Wrapping

Error wrapping involves adding context to an existing error. This helps in tracing the error back to its origin and provides more detailed information for debugging. Go 1.13 introduced standard library support for error wrapping using fmt.Errorf with the %w verb.

Basic Error Wrapping Example

This code demonstrates a simple error wrapping scenario. The openFile function attempts to open a file. If an error occurs (e.g., the file doesn't exist), it wraps the original os.Open error with additional context using fmt.Errorf and the %w verb. The errors.Is function is then used in main to check if the wrapped error is of type os.ErrNotExist.

package main

import (
	"errors"
	"fmt"
	"os"
)

func openFile(filename string) error {
	_, err := os.Open(filename)
	if err != nil {
		return fmt.Errorf("failed to open file %s: %w", filename, err)
	}
	return nil
}

func main() {
	err := openFile("nonexistent_file.txt")
	if err != nil {
		fmt.Println(err)
		if errors.Is(err, os.ErrNotExist) {
			fmt.Println("File does not exist.")
		}
	}
}

Concepts Behind the Snippet

  • Error Wrapping: Adding context to an existing error to provide more information about its origin.
  • fmt.Errorf with %w: The standard way to wrap errors in Go 1.13 and later. The %w verb embeds the original error within the new error.
  • errors.Is: A function to check if an error (or any error in its chain of wrapped errors) matches a specific error type.

Real-Life Use Case Section

Imagine a web application. When a database query fails, you might wrap the database error with information about the specific query that failed, the user ID, and other relevant details. This makes debugging much easier.

Best Practices

  • Always wrap errors: Add context whenever an error is passed up the call stack.
  • Use descriptive messages: Provide clear and informative messages when wrapping errors.
  • Avoid redundant wrapping: Don't wrap errors unnecessarily; only add context that is genuinely helpful.

Interview Tip

Be prepared to discuss the benefits of error wrapping and how it improves error handling in Go. Understand the difference between error wrapping and simply returning a new error.

When to Use Them

Use error wrapping when you want to add context to an error without losing the original error's information. This is particularly useful when propagating errors across different layers of your application.

Alternatives

Before Go 1.13, libraries like github.com/pkg/errors were commonly used for error wrapping. While still viable, the standard library's fmt.Errorf with %w is generally preferred for new projects.

Pros

  • Improved Debugging: Provides a clearer error trail.
  • Preserves Original Error: Allows you to check the original error type.
  • Standard Library Support: Built-in support simplifies error handling.

Cons

  • Increased Complexity: Requires careful consideration of error wrapping strategy.
  • Potential for Overwrapping: Adding too much context can make errors verbose and difficult to read.

More Complex Error Wrapping with Custom Errors

This example extends the concept by using a custom error type CustomError. The doSomething function returns a CustomError. The handleSomething function wraps this custom error. The errors.As function is then used in main to extract the CustomError from the wrapped error chain, allowing you to access its specific fields like the Code.

package main

import (
	"errors"
	"fmt"
)

type CustomError struct {
	Message string
	Code    int
}

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

func doSomething() error {
	return &CustomError{Message: "Something went wrong", Code: 500}
}

func handleSomething() error {
	err := doSomething()
	if err != nil {
		return fmt.Errorf("failed to handle something: %w", err)
	}
	return nil
}

func main() {
	err := handleSomething()
	if err != nil {
		fmt.Println(err)
		var customErr *CustomError
		if errors.As(err, &customErr) {
			fmt.Printf("Custom error code: %d\n", customErr.Code)
		}
	}
}

FAQ

  • What is the difference between errors.Is and errors.As?

    errors.Is checks if an error or any error in its chain of wrapped errors matches a specific error value (using ==). errors.As checks if an error or any error in its chain can be converted to a specific type, filling in a target variable with the converted error if it can. Use errors.Is for error values (like os.ErrNotExist) and errors.As for error types (like *CustomError).
  • Why use error wrapping instead of just returning a new error?

    Error wrapping preserves the original error's information, allowing you to inspect the root cause of the error. Simply returning a new error loses this context, making debugging more difficult.