Error wrapping allows you to add context to errors while preserving the original error. This is essential for building error chains that can be inspected and handled appropriately.
The %w verb in fmt.Errorf wraps an error:
if err != nil {
return fmt.Errorf("failed to process: %w", err)
}
Error wrapping serves multiple important purposes in Go programs. By adding context about where an error occurred, wrapping transforms a generic "file not found" error into "failed to read config: file not found," immediately clarifying the operation that failed. This context becomes invaluable when debugging, as error messages tell a story about the call chain that led to the failure.
Wrapping also preserves the original error within the chain, allowing you to inspect the full error history. This preservation enables sophisticated error handling: you can check for specific error types anywhere in the chain using errors.Is and errors.As, making it possible to handle particular error conditions even when they've been wrapped multiple times. This capability is essential for building robust error handling that can distinguish between different failure modes while maintaining rich diagnostic information.
Use errors.Unwrap to get the original error:
import "errors"
originalErr := errors.Unwrap(wrappedErr)
errors.Is: Checks if an error or any error in its chain matches a target:
if errors.Is(err, targetErr) {
// err or any error in its chain matches targetErr
}
errors.As: Checks if an error or any error in its chain is of a specific type:
var targetErr *MyError
if errors.As(err, &targetErr) {
// err or any error in its chain is of type *MyError
// targetErr now contains the matched error
}
Complete example
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
var ErrInvalidInput = errors.New("invalid input")
func findUser(id int) (string, error) {
if id < 0 {
return "", fmt.Errorf("findUser: %w", ErrInvalidInput)
}
if id == 0 {
return "", fmt.Errorf("findUser: user %w", ErrNotFound)
}
return fmt.Sprintf("user-%d", id), nil
}
func processUser(id int) error {
name, err := findUser(id)
if err != nil {
return fmt.Errorf("processUser: %w", err)
}
fmt.Println("Processing:", name)
return nil
}
func main() {
err := processUser(-1)
if err != nil {
fmt.Println("Error:", err)
// Check if error chain contains ErrInvalidInput
if errors.Is(err, ErrInvalidInput) {
fmt.Println("Invalid input detected")
}
}
}
Sentinel errors are predefined error values that represent specific error conditions. They're called "sentinels" because they act as markers that you can check for using errors.Is, even when the error has been wrapped multiple times.
The key characteristic of sentinel errors is that they're typically package-level variables defined using errors.New:
var ErrNotFound = errors.New("not found")
var ErrInvalidInput = errors.New("invalid input")
var ErrPermissionDenied = errors.New("permission denied")
Sentinel errors are useful because they allow you to check for specific error conditions without needing to know the exact error message or type. When you use errors.Is(err, ErrNotFound), it will find ErrNotFound anywhere in the error chain, even if it's been wrapped with additional context.
Best practices for sentinel errors
Err prefix (e.g., ErrNotFound, ErrInvalidInput)errors.New: Create them once as package-level variablesfmt.Errorf with %w to add context while preserving the sentinelpackage main
import (
"errors"
"fmt"
)
// Sentinel errors - predefined error values
var (
ErrNotFound = errors.New("not found")
ErrInvalidInput = errors.New("invalid input")
ErrUnauthorized = errors.New("unauthorized")
)
func findUser(id int) (string, error) {
if id < 0 {
// Wrap sentinel error with context
return "", fmt.Errorf("findUser: invalid id %d: %w", id, ErrInvalidInput)
}
if id == 0 {
return "", fmt.Errorf("findUser: user %w", ErrNotFound)
}
return fmt.Sprintf("user-%d", id), nil
}
func main() {
_, err := findUser(-1)
if err != nil {
// Check for sentinel error even though it's wrapped
if errors.Is(err, ErrInvalidInput) {
fmt.Println("Handling invalid input error")
}
}
}
Sentinel errors work particularly well with errors.Is because they can be checked for anywhere in an error chain. This makes them ideal for representing common error conditions that multiple functions might encounter.
Sometimes you need to collect and return multiple errors together. For example, when validating multiple fields, you might want to report all validation errors, not just the first one. Go 1.20 introduced errors.Join for this purpose.
errors.Join combines multiple errors into a single error. When you print the joined error, it shows all the individual errors. You can also use errors.Is and errors.As with joined errors to check for specific errors within the collection.
package main
import (
"errors"
"fmt"
)
func validateUser(name, email string) error {
var errs []error
if name == "" {
errs = append(errs, errors.New("name is required"))
}
if email == "" {
errs = append(errs, errors.New("email is required"))
}
if len(email) > 0 {
hasAt := false
for _, char := range email {
if char == '@' {
hasAt = true
break
}
}
if !hasAt {
errs = append(errs, errors.New("email must contain @"))
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func main() {
err := validateUser("", "invalid-email")
if err != nil {
fmt.Println("Validation errors:")
fmt.Println(err)
// Output:
// Validation errors:
// name is required
// email must contain @
// Check for specific errors in the collection
if errors.Is(err, errors.New("name is required")) {
fmt.Println("Name validation failed")
}
}
}
errors.Join is particularly useful in scenarios where you want to collect all errors before returning, such as: