ez

package module
v1.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 16, 2026 License: MIT Imports: 10 Imported by: 79

README

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.

Documentation

Index

Constants

View Source
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"     // requester does not have permissions to perform action
	ENOTAUTHENTICATED  = "not_authenticated"  // requester is not authenticated
	ERESOURCEEXHAUSTED = "resource_exhausted" // rate limit / quota exhausted
	ENOTIMPLEMENTED    = "not_implemented"    // the operation has not been 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
)

Application error codes

View Source
const StatusClientClosedRequest = 499

StatusClientClosedRequest is nginx's non-standard status for requests canceled by the client. net/http has no constant for it.

Variables

This section is empty.

Functions

func ErrorCode

func ErrorCode(err error) string

ErrorCode returns the code from a direct ez error chain, if available. Timeouts and cancellations are detected through standard error wrappers so they don't get misreported as internal errors. Otherwise returns EINTERNAL.

func ErrorData added in v1.4.0

func ErrorData(err error) map[string]interface{}

ErrorData returns the data from a direct ez error chain, if available.

func ErrorMessage

func ErrorMessage(err error) string

ErrorMessage returns the human-readable message of the error, if available. Otherwise returns a generic message: timeouts and cancellations get code-specific text, everything else the internal-error fallback.

func ErrorStacktrace added in v1.1.2

func ErrorStacktrace(err error) string

ErrorStacktrace returns a human-readable stacktrace of all nested errors, one per line: structured ez frames until the first foreign error, whose complete text is appended once. The trace is deliberately lossy past that point — ez frames below a foreign wrapper appear inside its flat text, not as structured lines.

func ErrorToGRPCCode added in v1.0.1

func ErrorToGRPCCode(err error) codes.Code

ErrorToGRPCCode converts an standar application error code to a GRPC error code

func ErrorToHTTPStatus added in v1.1.0

func ErrorToHTTPStatus(err error) int

ErrorToHTTPStatus converts an standar application error code to a HTTP status

func GRPCCodeToError added in v1.1.0

func GRPCCodeToError(c codes.Code) string

GRPCCodeToError converts a GRPC error code to a standar application error code

func HTTPStatusToError added in v1.1.0

func HTTPStatusToError(status int) string

HTTPStatusToError converts a HTTP status code to a standar application error code. Only statuses that indicate an unavailable service map to EUNAVAILABLE. Other 5xx statuses remain EINTERNAL because retryability is not implied.

Types

type Error

type Error struct {
	// Machine readable code
	Code string `json:"code"`
	// Human readable message
	Message string `json:"message"`
	// Logical operation
	Op string `json:"op"`
	// Nested error
	Err error `json:"err"`
	// Data about the error
	Data map[string]interface{} `json:"data,omitempty"`
}

Error defines a standar application error

func New

func New(code, message string, err error) *Error

New creates and returns a new error. The operation is derived from the calling function: "pkg.Type.Method" or "pkg.Function".

func NewFromGRPC added in v1.0.1

func NewFromGRPC(err error) *Error

NewFromGRPC wraps a GRPC Error into a standar application error. The operation is derived from the calling function. Status descriptions remain in Err and are never copied into the end-user-facing Message.

func Root added in v1.4.0

func Root(code, message string) *Error

Root creates a new root error. The operation is derived from the calling function.

func Wrap

func Wrap(err error) *Error

Wrap returns a new error that contains the passed error, useful for creating stacktraces. The operation is derived from the calling function.

func (*Error) AddData added in v1.4.0

func (e *Error) AddData(key string, value interface{}) *Error

WithData adds a single key-value pair to the error's data

func (*Error) AddDataMap added in v1.4.0

func (e *Error) AddDataMap(data map[string]interface{}) *Error

WithDataMap adds multiple key-value pairs to the error's data

func (*Error) Error

func (e *Error) Error() string

Error returns the string representation of the error message.

func (*Error) String added in v1.1.2

func (e *Error) String() string

String returns a simplified string representation of the error message

func (*Error) Unwrap added in v1.6.0

func (e *Error) Unwrap() error

Unwrap returns the nested error, so errors.Is and errors.As can traverse chains that mix *Error with standard library wrappers.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL