A Go library for enhanced error handling with stack traces, structured error information, and error wrapping capabilities.
- Stack Traces: Automatic capture of function location, file, line number, and timestamp
- Error Wrapping: Chain errors with causes for better error context
- Structured Errors: Add identifiers, details, and arbitrary properties to errors
- Method Chaining: Fluent API for building detailed error information
- Standard Library Compatible: Implements standard error interface and works with
errors.Is(),errors.As(), anderrors.Unwrap() - JSON Serialization: Built-in JSON marshaling for logging and debugging
go get github.com/scality/go-errorspackage main
import (
"fmt"
"github.com/scality/go-errors"
)
func main() {
var ErrDB = errors.New("database error")
err := errors.From(ErrDB).
WithIdentifier(1001).
WithDetail("connection timeout").
WithProperty("host", "localhost").
Throw()
fmt.Println(err)
}// Initializing Domain errors
var (
ErrValidationFailed = errors.New("validation failed")
ErrRequestFailed = errors.New("request failed")
ErrDatabaseError = errors.New("database error")
)
// Multiple details
// Each WithDetail() call appends to the Details slice
err1 := errors.From(ErrValidationFailed).
WithDetail("email is required").
WithDetail("password must be at least 8 characters").
Throw()
// Details can also be formatted
err2 := errors.From(ErrRequestFailed).
WithDetailf("failed to connect to %s:%d", "api.example.com", 443).
WithDetail("timeout after 30 seconds").
Throw()
// Single property
err3 := errors.From(ErrRequestFailed).
WithProperty("url", "https://api.example.com").
WithProperty("status_code", 500).
Throw()
// Multiple properties at once
err4 := errors.From(ErrDatabaseError).
WithProperties(map[string]any{
"host": "localhost",
"port": 5432,
"database": "myapp",
}).
Throw()var ErrUserNotFound = errors.New("user not found")
func getUserByID(id string) (*User, error) {
user, err := db.Query(id)
if err != nil {
return nil, errors.From(ErrUserNotFound).
WithIdentifier(404000).
CausedBy(err).
Throw()
}
return user, nil
}
// Convenient wrapping with Wrap() - adds message and stack trace
func getUser(id int) (*User, error) {
user, err := db.Query(id)
if err != nil {
return nil, errors.Wrap(err, "failed to fetch user from database")
}
return user, nil
}
// Convenient wrapping with Wrapf() - adds formatted message and stack trace
func getUser(id int) (*User, error) {
user, err := db.Query(id)
if err != nil {
return nil, errors.Wrapf(err, "failed to fetch user with id %d", id)
}
return user, nil
}var ErrSomethingWentWrong = errors.New("something went wrong")
func layer3() error {
return errors.From(ErrSomethingWentWrong).Throw()
}
func layer2() error {
err := layer3()
if err != nil {
return errors.Stamp(err) // Adds layer2's location to stack
}
return nil
}
func layer1() error {
err := layer2()
if err != nil {
return errors.Stamp(err) // Adds layer1's location to stack
}
return nil
}
// The error will contain a complete stack trace through all layers// Using errors.Is for comparison
notFoundErr := errors.New("not found")
if errors.Is(err, notFoundErr) {
// Handle not found error
}
// Using errors.As for type assertion
var e *errors.Error
if errors.As(err, &e) {
fmt.Printf("Error ID: %d\n", e.Identifier)
fmt.Printf("Details: %v\n", e.Details) // []string
fmt.Printf("Properties: %v\n", e.Properties)
// Access individual details
for i, detail := range e.Details {
fmt.Printf(" Detail %d: %s\n", i, detail)
}
}// Using From() to convert any error
stdErr := fmt.Errorf("something went wrong")
err := errors.From(stdErr).
WithDetail("additional context").
Throw()
// Using Intercept() to complete the error
func handleError(err error) error {
e := errors.Intercept(err)
e.WithProperty("handled_at", time.Now())
return e.Throw()
}The Error() method produces output in the following format:
title (id): detail1: detail2: detail3: key1='value1', key2='value2', at=(func='funcName', file='file.go', line='10'), caused by: underlying error
Note: Details are stored as a slice and joined with : when the error is formatted. Each call to WithDetail() or WithDetailf() appends to this slice.
Example with multiple details:
database error (1001): connection timeout: retry limit exceeded: host='localhost', port='5432', at=(func='connectDB', file='db.go', line='42'), caused by: dial tcp: connection refused
Example with wrapped error:
unknown error (0): failed to fetch user from database: at=(func='getUser', file='user.go', line='25'), caused by: connection refused
- Always use
Throw()when returning errors to capture stack traces - Use
Stamp()when passing errors up the call stack to track the error path - Use
Wrap()orWrapf()for convenient error wrapping with automatic stack traces - Use identifiers for errors that need programmatic handling (e.g., HTTP status codes)
- Add multiple details using
WithDetail()orWithDetailf()- they're stored as a slice and displayed in order - Add properties for debugging context (IDs, URLs, parameters, etc.)
- Wrap errors with
CausedBy()to maintain error chains for programmatic inspection - Use
From()when you need to enhance third-party errors with additional context - Use
Intercept()when you need to add context to errors from existing errors
- Details (slice of strings): Human-readable context that appears in error messages, ordered and concatenated with
: - Properties (key-value map): Structured data for debugging/logging, useful for searching and filtering logs
See LICENSE file for details.