ez is a minimalistic Go package for error handling, that makes errors a first-class citizen in your application domain.
It provides a clean, easy (pun intended) and consistent way to handle errors across different consumer roles: your application logic, end users and developers.
Based on Ben Johnson Failure is your domain awesome post.
Go's error handling can be challenging - while errors are core to the language, there's no prescribed way to handle them effectively. ez solves this by providing:
-
Role-Based Error Handling: Different error information for different consumers
- π€ Application: Clean error codes for programmatic handling
- π€ End Users: Clear, actionable error messages
- π¨π» Developers: Detailed logical stack traces for debugging
-
Domain-Centric Design: Errors become part of your domain model, just like your
CustomerorOrdertypes -
Clean Stack Traces: Logical operation tracking without the noise of full stack traces
-
Standard Error Codes: Pre-defined, widely-applicable error codes inspired by HTTP/gRPC standards
go get github.com/vanclief/ezUpgrade guides live in the docs folder.
import "github.com/vanclief/ez"
// Create a new error
err := ez.New(
ez.EINVALID, // Error code
"Username cannot be empty", // User-friendly message
nil, // Optional underlying error
)
// The operation name is derived automatically from the calling function,
// e.g. "users.Service.CreateUser" β no need to declare it.
// Check error codes
if ez.ErrorCode(err) == ez.EINVALID {
// Handle validation error
}
// Get user-friendly message
message := ez.ErrorMessage(err) // "Username cannot be empty"
// Get the full error trace for developers
trace := ez.ErrorStacktrace(err) // users.Service.CreateUser <invalid> "Username cannot be empty"Pre-defined error codes that cover most common scenarios:
const (
ECONFLICT = "conflict" // Request is valid but the current state of the data forbids it
EINTERNAL = "internal" // Unexpected internal failure where retry is not implied
EINVALID = "invalid" // Validation failed
ENOTFOUND = "not_found" // Entity does not exist
ENOTAUTHORIZED = "not_authorized" // Missing permissions
ENOTAUTHENTICATED = "not_authenticated" // Not authenticated
ERESOURCEEXHAUSTED = "resource_exhausted" // Rate limit / quota exhausted
ENOTIMPLEMENTED = "not_implemented" // Not implemented
EUNAVAILABLE = "unavailable" // The operation is unavailable and retry may help
ETIMEOUT = "timeout" // The operation ran out of time
ECANCELED = "canceled" // The caller gave up before the operation finished
)ErrorCode detects timeouts and cancellations in wrapped non-ez errors β
the context.Canceled, context.DeadlineExceeded and
os.ErrDeadlineExceeded sentinels anywhere in the chain, plus a Timeout() bool check on the top-level error only β so ez.Wrap(err) on a failed
HTTP call yields ETIMEOUT instead of misreporting EINTERNAL.
ErrorToHTTPStatus / ErrorToGRPCCode convert ez codes for outbound
responses; HTTPStatusToError / NewFromGRPC classify inbound responses.
Mappings preserve recovery semantics rather than encoding who caused the
failure. HTTP 502/503 and gRPC Unavailable map to EUNAVAILABLE; HTTP 500,
other unmapped 5xx statuses and gRPC Internal map to EINTERNAL. HTTP
501 maps to ENOTIMPLEMENTED, 504/408 to ETIMEOUT, 410 to ENOTFOUND,
412 to ECONFLICT, 499 to ECANCELED, and unmapped 4xx statuses to
EINVALID. The two directions are deliberately not exact inverses because
several transport statuses can share one application classification.
NewFromGRPC gives an explicit gRPC status precedence over context sentinels
elsewhere in the error chain. It never copies an upstream status description
into the end-user-facing Message; the original diagnostic remains available
through the nested error and ErrorMessage returns a safe fallback. Callers
must explicitly provide any trusted, sanitized end-user message.
Build logical stack traces by wrapping errors. Every constructor derives the
operation name from the function that calls it ("pkg.Type.Method" for methods,
"pkg.Function" for functions), so there is nothing to declare or keep in sync.
*Error implements Unwrap, so errors.Is and errors.As traverse through
ez errors into their nested causes.
ErrorCode, ErrorMessage and ErrorData read direct ez chains only; they do
not recover ez metadata hidden behind a non-ez wrapper. Use ez.Wrap instead
of fmt.Errorf("...: %w", err) when propagating an ez error.
func (s *UserService) CreateUser(ctx context.Context, user *User) error {
// Validate user
if user.Username == "" {
return ez.New(ez.EINVALID, "Username is required", nil)
// Op: "users.UserService.CreateUser"
}
// Try to create user
if err := s.db.CreateUser(user); err != nil {
return ez.Wrap(err) // Preserves original error details
}
return nil
}Attach additional contextual data to errors:
// Add single data field
err := ez.Root(ez.EINVALID, "Invalid user data").
AddData("user_id", "123")
// Add multiple data fields at once
err := ez.Root(ez.ECONFLICT, "User already exists").
AddDataMap(map[string]interface{}{
"username": user.Username,
"email": user.Email,
})
// Access error data
data := ez.ErrorData(err) // Returns map[string]interface{}
userID := data["user_id"].(string)Data is preserved when wrapping errors:
err := ez.Root(ez.ENOTFOUND, "User not found").
AddData("user_id", "123")
wrappedErr := ez.Wrap(err)
data := ez.ErrorData(wrappedErr) // Still contains "user_id"Easy access to error details:
// Get error code
code := ez.ErrorCode(err) // e.g., "invalid"
// Get user message
msg := ez.ErrorMessage(err) // e.g., "Username is required"
// Get the full error trace (for developers)
trace := ez.ErrorStacktrace(err) // users.UserService.CreateUser <invalid> "Username is required"Here's an example showing how to handle errors with ez:
func (s *UserService) CreateUser(ctx context.Context, user *User) error {
// Validation error (end user focused)
if user.Username == "" {
return ez.New(ez.EINVALID, "Username is required", nil)
}
// Check for conflicts (application logic focused)
exists, err := s.checkUserExists(user.Username)
if err != nil {
return ez.Wrap(err) // Wraps internal error for developers
}
if exists {
return ez.New(ez.ECONFLICT,
"Username is already taken. Please choose another one.", nil).AddData("username", user.Username)
}
// Database error (developer focused)
if err := s.db.CreateUser(user); err != nil {
return ez.Wrap(err)
}
return nil
}user := &User{Username: ""}
err := svc.CreateUser(ctx, user)
// Application logic
switch ez.ErrorCode(err) {
case ez.EINVALID:
// Handle validation error
case ez.ECONFLICT:
// Handle conflict error
case ez.EINTERNAL:
// Handle internal error
}
// End user message
if err != nil {
fmt.Println("Error:", ez.ErrorMessage(err))
// Output: "Error: Username is required"
data := ez.ErrorData(err)
if username, ok := data["username"].(string); ok {
// Return specific username error
}
}
// Developer debugging
if err != nil {
fmt.Println(ez.ErrorStacktrace(err))
// Output: users.UserService.CreateUser <invalid> "Username is required"
}Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.