Skip to content

Latest commit

Β 

History

26 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

ez

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.

Why ez?

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 or Order 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

Installation

go get github.com/vanclief/ez

Upgrade guides live in the docs folder.

Quick Start

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"

Core Features

1. Standardized Error Codes

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.

HTTP and gRPC mappings

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.

2. Error Wrapping

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
}

3. Error Data

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"

4. Error Information Extraction

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"

Example

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
}

Handling the Error

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"
}

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

Minimalistic package for handling Go errors in an easy way

Topics

Resources

Stars

13 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages