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
Customer
orOrder
types -
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/ez
import "github.com/vanclief/ez"
// Create a new error
err := ez.New(
"UserService.CreateUser", // Operation name
ez.EINVALID, // Error code
"Username cannot be empty", // User-friendly message
nil, // Optional underlying error
)
// 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 full error trace for developers
ez.ErrorStackTrace(err) // "UserService.CreateUser: <invalid> Username cannot be empty"
Pre-defined error codes that cover most common scenarios:
const (
ECONFLICT = "conflict" // Action cannot be performed
EINTERNAL = "internal" // Internal error
EINVALID = "invalid" // Validation failed
ENOTFOUND = "not_found" // Entity does not exist
ENOTAUTHORIZED = "not_authorized" // Missing permissions
ENOTAUTHENTICATED = "not_authenticated" // Not authenticated
ERESOURCEEXHAUSTED = "resource_exhausted" // Resource exhausted
ENOTIMPLEMENTED = "not_implemented" // Not implemented
EUNAVAILABLE = "unavailable" // System unavailable
)
Build logical stack traces by wrapping errors:
func (s *UserService) CreateUser(ctx context.Context, user *User) error {
const op = "UserService.CreateUser"
// Validate user
if user.Username == "" {
return ez.New(op, ez.EINVALID, "Username is required", nil)
}
// Try to create user
if err := s.db.CreateUser(user); err != nil {
return ez.Wrap(op, err) // Preserves original error details
}
return nil
}
Attach additional contextual data to errors:
Copy// Add single data field
err := ez.NewRoot(op, ez.EINVALID, "Invalid user data").
AddData("user_id", "123")
// Add multiple data fields at once
err := ez.NewRoot(op, 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.NewRoot(op, ez.ENOTFOUND, "User not found").
AddData("user_id", "123")
wrappedErr := ez.Wrap("UserService.GetUser", 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 full error trace (for developers)
ez.ErrorStacktrace(err) // e.g., "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 {
const op = "UserService.CreateUser"
// Validation error (end user focused)
if user.Username == "" {
return ez.New(op, ez.EINVALID, "Username is required", nil)
}
// Check for conflicts (application logic focused)
exists, err := s.checkUserExists(user.Username)
if err != nil {
return ez.Wrap(op, err) // Wraps internal error for developers
}
if exists {
return ez.New(op, 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(op, 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 already taken"
if username, ok := data["username"].(string); ok {
// Return specific username error
}
}
// Developer debugging
if err != nil {
ez.ErrorStacktrace(err)
// Output: "UserService.CreateUser: <invalid> Username is already taken"
}
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.