Package wrapperrors provides primitives for creating and handling errors in Go. It enhances standard error handling by allowing errors to carry detailed, structured information like multiple codes, messages, and HTTP status codes, while remaining fully compatible with Go's standard error functions (errors.Is, errors.As, errors.Unwrap). It also offers custom formatting options for detailed logging.
go get github.com/felipewom/go-wrapperrors- Define reusable error types with associated codes and default HTTP status codes.
- Create error instances with detailed context including multiple messages and status codes.
- Wrap existing errors (standard or
ErrorWrappertypes) to add context while preserving the original error chain. - Full compatibility with standard Go error handling:
errors.Is(err, target)errors.As(err, &targetInterfaceOrType)errors.Unwrap(err)
- Custom error formatting using
fmt.Formatter:%s,%v: Standard error message.%+v: Detailed, JSON-like structured output.
- Access structured error details programmatically (
GetCodes(),GetMessages(),GetStatuses(),Json()).
First, define the types of errors your application will use. This is typically done as global variables using wrapperrors.Define(code string, defaultStatus int).
Example: Create a package apperrors for your application's error definitions.
package errorsdefinitions
import (
"github.com/felipewom/go-wrapperrors/wrapperrors"
"net/http"
)
var (
Internal = wrapperrors.Define("internal_error", http.StatusInternalServerError)
NotFound = wrapperrors.Define("not_found", http.StatusNotFound)
InvalidPayload = wrapperrors.Define("invalid_payload", http.StatusBadRequest)
Unauthorized = wrapperrors.Define("unauthorized_access", http.StatusUnauthorized)
)Create instances from your predefined errors and add context using WithCause(error) and WithMessage(string).
Example: Returning a NotFound error if a database record isn't found.
// file: services/carservice.go
package services
import (
"database/sql"
"errors" // Standard errors package
"fmt"
"github.com/your_username/your_project/apperrors" // Your error definitions
)
type Car struct {
ID string
Name string
}
// GetCar retrieves a car by its ID.
func GetCar(id string) (Car, error) {
var car Car
// Simulating database call
// err := db.QueryRow("SELECT id, name FROM car WHERE id = ?", id).Scan(&car.ID, &car.Name)
err := sql.ErrNoRows // Simulate error for example
if err == nil {
return car, nil
}
if errors.Is(err, sql.ErrNoRows) {
// Create an instance of apperrors.NotFound, wrap sql.ErrNoRows, and add a descriptive message.
return Car{}, apperrors.NotFound.WithCause(err).WithMessage(fmt.Sprintf("Car with ID '%s' not found in the database", id))
}
// For other unexpected database errors, wrap with a general internal error.
return Car{}, apperrors.Internal.WithCause(err).WithMessage("Failed to retrieve car from database due to an unexpected issue")
}You can add more context to an existing error. If it's an ErrorWrapper, you can use its methods. If it's a standard error, you'll wrap it.
func ProcessCar(carID string) error {
car, err := GetCar(carID)
if err != nil {
// Assuming GetCar returns an ErrorWrapper compatible error
if ew, ok := err.(wrapperrors.ErrorWrapper); ok {
// Add more specific context to the error returned by GetCar
return ew.WithMessage(fmt.Sprintf("Processing failed for car ID %s", carID))
}
// If it's not an ErrorWrapper, but you want to make it one:
return apperrors.Internal.WithCause(err).WithMessage(fmt.Sprintf("Processing failed for car ID %s", carID))
}
// ... process car
return nil
}
// If you need to wrap a standard error or change the fundamental cause:
func AnotherProcess(id string) error {
stdErr := errors.New("a standard library error occurred")
// some condition ...
if stdErr != nil {
// Wrap the standard error with one of your defined types
return apperrors.InvalidPayload.WithCause(stdErr).WithMessage("Standard error occurred during processing")
}
return nil
}Use errors.Is from the standard Go library to check if an error matches one of your predefined types or any error in its wrap chain. wrapperrors.ErrorWrapper is designed to work seamlessly with it.
// file: main.go
package main
import (
"errors" // Standard errors package
"fmt"
"log" // Standard log package
"github.com/your_username/your_project/apperrors" // Your error definitions
"github.com/your_username/your_project/services" // Your services
)
func main() {
_, err := services.GetCar("123") // Example call
if err != nil {
if errors.Is(err, apperrors.NotFound) {
fmt.Println("Handler: The requested car was not found.")
// Handle NotFound specifically (e.g., return 404 to client)
} else if errors.Is(err, apperrors.Internal) {
fmt.Println("Handler: An unexpected internal error occurred.")
// Handle Internal specifically
} else {
fmt.Println("Handler: An unknown error occurred.")
}
// Log the detailed error
log.Printf("Detailed error for logging: %+v\n", err)
}
}The package-level wrapperrors.Is(err, target) is also available but using errors.Is directly is generally recommended for consistency with standard Go practices.
ErrorWrapper implements the Unwrap() error method, making it fully compatible with errors.As for retrieving a specific error type from the chain and errors.Unwrap for unwrapping layers of errors.
type CustomDatabaseError struct {
Query string
OriginalError error
}
func (cde *CustomDatabaseError) Error() string { return fmt.Sprintf("db error with query '%s': %v", cde.Query, cde.OriginalError) }
func (cde *CustomDatabaseError) Unwrap() error { return cde.OriginalError }
// ...
// var someErr error
// Assume 'someErr' is an error chain that might include an ErrorWrapper,
// which in turn wraps a *CustomDatabaseError.
// For example:
// originalDBErr := &CustomDatabaseError{Query: "SELECT *", OriginalError: errors.New("connection failed")}
// someErr = apperrors.Internal.WithCause(originalDBErr).WithMessage("Operation failed")
var cde *CustomDatabaseError
// if errors.As(someErr, &cde) {
// fmt.Printf("Custom DB Error (Query: %s) found in chain!\n", cde.Query)
// }
// Unwrapping layer by layer
// unwrapped := errors.Unwrap(someErr)
// if unwrapped != nil {
// fmt.Printf("Unwrapped error: %v\n", unwrapped)
// }ErrorWrapper provides multiple ways to access error information:
-
err.Error()orfmt.Sprintf("%s", err)orfmt.Sprintf("%v", err):- Returns the standard error message. If the error wraps other errors using
WithCause, their messages are included in a chain (e.g., "wrapper message: cause message: deeper cause message"). - Example:
codes:not_found,messages:Car with ID '123' not found in the database: sql: no rows in result set
- Returns the standard error message. If the error wraps other errors using
-
err.String()orfmt.Sprintf("%+v", err):- Returns a detailed, JSON-like string representation, including codes, messages, statuses, and a recursively formatted cause. Useful for verbose logging.
- Example:
{"code": ["not_found"], "message": ["Car with ID '123' not found in the database"], "status": [{"message": "Not Found", "code": 404}], "cause": "sql: no rows in result set"} - (If the cause were also an
ErrorWrapper, its details would be nested similarly.)
-
err.Json() map[string]interface{}:- Returns a map representation of the error, suitable for structured logging or programmatic access.
- Example structure:
(If the cause is an
{ "code": ["not_found"], "message": ["Car with ID '123' not found in the database"], "status": [ {"message": "Not Found", "code": 404} ], "cause": "sql: no rows in result set" }ErrorWrapper,causewill be a nested map; otherwise, it's the cause's error string.)
-
Accessor Methods:
ew.GetCodes() []string: Returns a slice of all codes (e.g.,[]string{"not_found"}).ew.GetMessages() []string: Returns a slice of all messages (e.g.,[]string{"Car with ID '123' not found"}).ew.GetStatuses() []statusCode: Returns a slice ofstatusCodestructs (each withMessagestring andCodeint fields).
Example Usage (assuming err is an ErrorWrapper):
// import "github.com/felipewom/go-wrapperrors/wrapperrors"
// import "log" // Standard log package
// ... inside a function where 'err' is an ErrorWrapper instance
// var err error = // ... some error
// if ew, ok := err.(wrapperrors.ErrorWrapper); ok {
// log.Printf("Standard error: %s\n", ew.Error())
// log.Printf("Detailed error: %+v\n", ew)
//
// errMap := ew.Json()
// log.Printf("Structured error: %v\n", errMap) // This will print the map using default formatting
//
// if codes := ew.GetCodes(); len(codes) > 0 {
// log.Printf("Primary error code: %s\n", codes[0])
// }
// } else if err != nil {
// log.Printf("Standard error (non-ErrorWrapper): %s\n", err.Error())
// }wrapperrors.New(code string, cause error): UseDefine(code, status).WithCause(cause).WithMessage(message)orPredefinedError.WithCause(cause).WithMessage(message)instead for clearer error construction and explicit status definition.wrapperrors.Wrap(e error, message string): To add a message to an existingErrorWrapper, useexistingErrorWrapper.WithMessage(message). To wrap a standard error or create a newErrorWrapperwith a cause, useDefine(...).WithCause(e).WithMessage(message)orPredefinedError.WithCause(e).WithMessage(message).
- Felipe Moura · felipewom@gmail.com