Documentation
¶
Overview ¶
Package errors provides a foundational error handling system. It extends Go's standard error handling with structured error codes, retry classification, context preservation, and API serialization capabilities.
Package errors provides structured error handling.
This package extends Go's standard error handling with error codes, classification (retryable vs permanent), context metadata, and JSON serialization. It maintains full compatibility with the standard library errors package (errors.Is, errors.As, errors.Unwrap).
Features ¶
- Structured error codes for consistent categorization
- Error classification for intelligent retry logic (retryable vs permanent)
- Context metadata attachment for debugging
- Error wrapping that preserves the error chain
- JSON serialization for API responses
- Zero dependencies (Layer 0 library)
Design Principles ¶
- Standard library compatibility (errors.Is, errors.As, errors.Unwrap)
- Immutability (errors are immutable once created)
- Type safety (strong types for codes and classifications)
- Simplicity (minimal API surface, easy to use correctly)
- Performance (optimized for error creation in hot paths)
Quick Start ¶
Creating errors:
// Simple error err := errors.New(errors.CodeNotFound, "user not found") // Formatted error err := errors.Newf(errors.CodeInvalidInput, "invalid age: %d", age)
Wrapping errors:
result, err := repo.Query(ctx, id)
if err != nil {
return errors.Wrap(err, errors.CodeDatabase, "failed to query user")
}
Adding context:
err := errors.New(errors.CodeBuildFailed, "build failed") err = errors.WithContext(err, "project", "api") err = errors.WithContext(err, "phase", "test")
Retry logic:
if errors.IsRetryable(err) {
// Implement retry with backoff
time.Sleep(backoff)
return retry(operation)
}
JSON serialization:
func handleError(w http.ResponseWriter, err error) {
response := errors.ToJSON(err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(getHTTPStatus(errors.GetCode(err)))
json.NewEncoder(w).Encode(response)
}
Error Codes ¶
The library provides predefined error codes for all common platform scenarios:
- Resource errors: CodeNotFound, CodeAlreadyExists, CodeConflict
- Permission errors: CodeUnauthorized, CodeForbidden
- Validation errors: CodeInvalidInput, CodeInvalidConfig, CodeSchemaFailed
- Infrastructure errors: CodeDatabase, CodeNetwork, CodeTimeout, CodeRateLimit
- Execution errors: CodeExecutionFailed, CodeBuildFailed, CodePublishFailed
- System errors: CodeInternal, CodeNotImplemented, CodeUnavailable
- Generic: CodeUnknown
Each error code has a default classification (retryable or permanent) that can be overridden when needed.
Error Classification ¶
Errors are classified as either retryable or permanent:
- Retryable: Temporary failures (network, timeout, rate limit, transient DB issues)
- Permanent: Logic errors (validation, not found, permission denied)
Use errors.IsRetryable(err) to make retry decisions. The classification is preserved when wrapping errors and can be overridden with WithClassification.
Standard Library Compatibility ¶
PlatformError implements the error interface and works seamlessly with standard library error functions:
// errors.Is traverses the error chain
if errors.Is(err, sql.ErrNoRows) {
// Handle no rows
}
// errors.As finds typed errors in the chain
var platformErr errors.PlatformError
if errors.As(err, &platformErr) {
code := platformErr.Code()
}
// errors.Unwrap retrieves the wrapped error
cause := errors.Unwrap(err)
Context Metadata ¶
Attach debugging context to errors without exposing sensitive information:
err := errors.New(errors.CodeBuildFailed, "build failed")
err = errors.WithContextMap(err, map[string]interface{}{
"project": "api",
"phase": "test",
"exit_code": 1,
"duration": "2m30s",
})
Context is included in JSON serialization but not in error chains exposed to external callers (security).
Best Practices ¶
- Always wrap errors with context: errors.Wrap(err, code, message)
- Use specific error codes, not CodeUnknown
- Don't include sensitive data in error messages or context
- Use IsRetryable for retry decisions, not specific codes
- Add context at each layer of the call stack
- Preserve classification when wrapping (automatic)
- Use ToJSON for API responses to prevent information leakage
Performance ¶
The library is optimized for minimal overhead:
- Error creation: <10μs
- Error wrapping: <5μs
- Context attachment: <2μs
- Classification lookup: O(1)
See benchmarks for detailed performance characteristics.
Example (Workflow) ¶
Example_workflow shows a complete error handling workflow across multiple layers.
package main
import (
"fmt"
"github.com/jmgilman/go/errors"
)
func main() {
// Layer 1: Database layer
dbErr := fmt.Errorf("connection timeout")
// Layer 2: Repository layer wraps and adds context
repoErr := errors.Wrap(dbErr, errors.CodeDatabase, "failed to query users")
repoErr = errors.WithContext(repoErr, "table", "users")
// Layer 3: Service layer wraps with business error
svcErr := errors.Wrap(repoErr, errors.CodeNotFound, "user not found")
svcErr = errors.WithContext(svcErr, "user_id", "12345")
// Check if retryable
fmt.Println("Retryable:", errors.IsRetryable(svcErr))
// Get error code
fmt.Println("Code:", errors.GetCode(svcErr))
// Serialize for API
response := errors.ToJSON(svcErr)
fmt.Println("API Code:", response.Code)
fmt.Println("API Message:", response.Message)
}
Output: Retryable: true Code: NOT_FOUND API Code: NOT_FOUND API Message: user not found
Index ¶
- func As(err error, target interface{}) bool
- func Is(err, target error) bool
- func IsRetryable(err error) bool
- type ErrorClassification
- type ErrorCode
- type ErrorResponse
- type PlatformError
- func New(code ErrorCode, message string) PlatformError
- func Newf(code ErrorCode, format string, args ...interface{}) PlatformError
- func WithClassification(err error, classification ErrorClassification) PlatformError
- func WithContext(err error, key string, value interface{}) PlatformError
- func WithContextMap(err error, ctx map[string]interface{}) PlatformError
- func Wrap(err error, code ErrorCode, message string) PlatformError
- func WrapWithContext(err error, code ErrorCode, message string, ctx map[string]interface{}) PlatformError
- func Wrapf(err error, code ErrorCode, format string, args ...interface{}) PlatformError
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func As ¶
As finds the first error in err's chain that matches target. This is a convenience wrapper around the standard library errors.As.
Example:
var platformErr PlatformError
if errors.As(err, &platformErr) {
code := platformErr.Code()
}
Example ¶
package main
import (
"fmt"
"github.com/jmgilman/go/errors"
)
func main() {
err := errors.New(errors.CodeTimeout, "request timeout")
wrapped := errors.Wrap(err, errors.CodeInternal, "internal error")
// Extract PlatformError from chain
var platformErr errors.PlatformError
if errors.As(wrapped, &platformErr) {
fmt.Println("Code:", platformErr.Code())
fmt.Println("Retryable:", platformErr.Classification().IsRetryable())
}
}
Output: Code: INTERNAL_ERROR Retryable: true
func Is ¶
Is reports whether any error in err's chain matches target. This is a convenience wrapper around the standard library errors.Is.
Example:
var ErrNotFound = errors.New(errors.CodeNotFound, "not found")
if errors.Is(err, ErrNotFound) {
// Handle not found case
}
Example ¶
package main
import (
"fmt"
"github.com/jmgilman/go/errors"
)
func main() {
// Create sentinel error
var ErrUserNotFound = errors.New(errors.CodeNotFound, "user not found")
// Wrap the sentinel
err := errors.Wrap(ErrUserNotFound, errors.CodeDatabase, "query failed")
// Check if error chain contains sentinel
if errors.Is(err, ErrUserNotFound) {
fmt.Println("User not found in chain")
}
}
Output: User not found in chain
func IsRetryable ¶
IsRetryable returns true if the error is classified as retryable. Returns false if the error is nil or not a PlatformError (safe default).
This is the primary function for making retry decisions in the platform. It provides a simple boolean check to determine if an operation should be retried after a failure.
Example:
if errors.IsRetryable(err) {
// Implement retry with backoff
time.Sleep(backoff)
return retry(operation)
}
Example ¶
package main
import (
"fmt"
"github.com/jmgilman/go/errors"
)
func main() {
// Retryable error
timeoutErr := errors.New(errors.CodeTimeout, "request timeout")
fmt.Println("Timeout retryable:", errors.IsRetryable(timeoutErr))
// Permanent error
notFoundErr := errors.New(errors.CodeNotFound, "user not found")
fmt.Println("NotFound retryable:", errors.IsRetryable(notFoundErr))
}
Output: Timeout retryable: true NotFound retryable: false
Example (RetryLoop) ¶
package main
import (
"fmt"
"time"
"github.com/jmgilman/go/errors"
)
func main() {
operation := func() error {
// Simulate operation that might fail
return errors.New(errors.CodeNetwork, "connection refused")
}
const maxRetries = 3
var err error
for attempt := 0; attempt < maxRetries; attempt++ {
err = operation()
if err == nil {
fmt.Println("Success")
return
}
if !errors.IsRetryable(err) {
fmt.Println("Permanent error, not retrying")
return
}
fmt.Printf("Attempt %d failed, retrying...\n", attempt+1)
time.Sleep(100 * time.Millisecond) // Backoff
}
fmt.Println("Max retries exceeded")
}
Output: Attempt 1 failed, retrying... Attempt 2 failed, retrying... Attempt 3 failed, retrying... Max retries exceeded
Types ¶
type ErrorClassification ¶
type ErrorClassification string
ErrorClassification indicates whether an error should trigger a retry. This is used by platform services to determine if an operation should be retried or if it represents a permanent failure.
const ( // ClassificationRetryable indicates temporary failures that may succeed on retry. // Examples: network timeouts, rate limits, transient database issues. ClassificationRetryable ErrorClassification = "RETRYABLE" // ClassificationPermanent indicates failures that will not succeed on retry. // Examples: validation errors, permission denials, resource not found. ClassificationPermanent ErrorClassification = "PERMANENT" )
func GetClassification ¶
func GetClassification(err error) ErrorClassification
GetClassification extracts the ErrorClassification from an error. Returns ClassificationPermanent if the error is nil or not a PlatformError. This is a safe default that prevents inappropriate retry attempts.
This function handles the error chain and will extract the classification from the outermost PlatformError in the chain.
Example:
classification := errors.GetClassification(err)
if classification == errors.ClassificationRetryable {
// Retry logic
}
func (ErrorClassification) IsRetryable ¶
func (c ErrorClassification) IsRetryable() bool
IsRetryable returns true if the classification indicates retry should be attempted.
type ErrorCode ¶
type ErrorCode string
ErrorCode represents a specific error condition. Error codes are string-based for debuggability and natural JSON serialization.
const ( // CodeNotFound indicates a requested resource does not exist. CodeNotFound ErrorCode = "NOT_FOUND" // CodeAlreadyExists indicates a resource already exists and cannot be created again. CodeAlreadyExists ErrorCode = "ALREADY_EXISTS" // CodeConflict indicates a resource state conflict that prevents the operation. CodeConflict ErrorCode = "CONFLICT" CodeUnauthorized ErrorCode = "UNAUTHORIZED" // CodeForbidden indicates the authenticated user lacks permission for the operation. CodeForbidden ErrorCode = "FORBIDDEN" // CodeInvalidInput indicates the provided input is invalid or malformed. CodeInvalidInput ErrorCode = "INVALID_INPUT" // CodeInvalidConfig indicates a configuration error prevents the operation. CodeInvalidConfig ErrorCode = "INVALID_CONFIGURATION" // CodeSchemaFailed indicates the data failed schema validation. CodeSchemaFailed ErrorCode = "SCHEMA_VALIDATION_FAILED" // CodeDatabase indicates a database operation failed. CodeDatabase ErrorCode = "DATABASE_ERROR" // CodeNetwork indicates a network operation failed. CodeNetwork ErrorCode = "NETWORK_ERROR" // CodeTimeout indicates an operation exceeded its time limit. CodeTimeout ErrorCode = "TIMEOUT" // CodeRateLimit indicates the rate limit has been exceeded. CodeRateLimit ErrorCode = "RATE_LIMIT_EXCEEDED" // CodeExecutionFailed indicates a general execution failure. CodeExecutionFailed ErrorCode = "EXECUTION_FAILED" // CodeBuildFailed indicates a build operation failed. CodeBuildFailed ErrorCode = "BUILD_FAILED" // CodePublishFailed indicates a publish operation failed. CodePublishFailed ErrorCode = "PUBLISH_FAILED" // CodeCUELoadFailed indicates CUE file/module loading failed. CodeCUELoadFailed ErrorCode = "CUE_LOAD_FAILED" // CodeCUEBuildFailed indicates CUE build/evaluation failed. CodeCUEBuildFailed ErrorCode = "CUE_BUILD_FAILED" // CodeCUEValidationFailed indicates CUE validation failed. CodeCUEValidationFailed ErrorCode = "CUE_VALIDATION_FAILED" // CodeCUEDecodeFailed indicates CUE to Go struct decoding failed. CodeCUEDecodeFailed ErrorCode = "CUE_DECODE_FAILED" // CodeCUEEncodeFailed indicates CUE to YAML/JSON encoding failed. CodeCUEEncodeFailed ErrorCode = "CUE_ENCODE_FAILED" // CodeSchemaVersionIncompatible indicates incompatible major schema version. // Config major version does not match supported schema major version. CodeSchemaVersionIncompatible ErrorCode = "SCHEMA_VERSION_INCOMPATIBLE" // CodeInternal indicates an internal system error occurred. CodeInternal ErrorCode = "INTERNAL_ERROR" // CodeNotImplemented indicates the requested functionality is not implemented. CodeNotImplemented ErrorCode = "NOT_IMPLEMENTED" CodeUnavailable ErrorCode = "SERVICE_UNAVAILABLE" // CodeUnknown indicates an unknown or unclassified error occurred. CodeUnknown ErrorCode = "UNKNOWN" )
func GetCode ¶
GetCode extracts the ErrorCode from an error. Returns CodeUnknown if the error is nil or not a PlatformError.
This function handles the error chain and will extract the code from the outermost PlatformError in the chain.
Example:
if errors.GetCode(err) == errors.CodeNotFound {
// Handle not found
}
Example ¶
package main
import (
"fmt"
"github.com/jmgilman/go/errors"
)
func main() {
err := errors.New(errors.CodeNotFound, "user not found")
if errors.GetCode(err) == errors.CodeNotFound {
fmt.Println("Handle not found case")
}
}
Output: Handle not found case
type ErrorResponse ¶
type ErrorResponse struct {
// Code is the error code identifying the type of error.
Code string `json:"code"`
// Message is the human-readable error message.
Message string `json:"message"`
// Classification indicates whether the error is retryable or permanent.
Classification string `json:"classification"`
// Context contains optional metadata about the error.
// Omitted from JSON if empty.
Context map[string]interface{} `json:"context,omitempty"`
}
ErrorResponse represents the JSON structure for error responses in API endpoints. It provides a flat, serializable representation of errors without exposing internal error chains or sensitive information.
The wrapped error chain is intentionally excluded to prevent information leakage while still providing useful debugging context through the Code, Message, and Context fields.
func ToJSON ¶
func ToJSON(err error) *ErrorResponse
ToJSON converts any error to an ErrorResponse suitable for JSON serialization. Returns nil if err is nil.
For PlatformError instances, extracts code, message, classification, and context. For standard errors, uses CodeUnknown, ClassificationPermanent, and the error message.
The wrapped error chain is intentionally excluded to prevent information leakage. Security consideration: Error chains may contain internal implementation details, stack traces, database queries, file paths, or other sensitive information.
Example:
func handleError(w http.ResponseWriter, err error) {
response := errors.ToJSON(err)
if response == nil {
return // No error
}
w.Header().Set("Content-Type", "application/json")
statusCode := getHTTPStatus(response.Code)
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(response)
}
Example ¶
package main
import (
"encoding/json"
"fmt"
"github.com/jmgilman/go/errors"
)
func main() {
err := errors.New(errors.CodeInvalidInput, "validation failed")
err = errors.WithContext(err, "field", "email")
err = errors.WithContext(err, "reason", "invalid format")
response := errors.ToJSON(err)
jsonBytes, _ := json.MarshalIndent(response, "", " ")
fmt.Println(string(jsonBytes))
}
Output: { "code": "INVALID_INPUT", "message": "validation failed", "classification": "PERMANENT", "context": { "field": "email", "reason": "invalid format" } }
Example (HttpHandler) ¶
package main
import (
"encoding/json"
"fmt"
"net/http"
"github.com/jmgilman/go/errors"
)
func main() {
// Example HTTP error handler
handleError := func(w http.ResponseWriter, err error) {
response := errors.ToJSON(err)
w.Header().Set("Content-Type", "application/json")
// Map error code to HTTP status
statusCode := http.StatusInternalServerError
switch errors.GetCode(err) {
case errors.CodeNotFound:
statusCode = http.StatusNotFound
case errors.CodeUnauthorized:
statusCode = http.StatusUnauthorized
case errors.CodeInvalidInput:
statusCode = http.StatusBadRequest
}
w.WriteHeader(statusCode)
_ = json.NewEncoder(w).Encode(response)
}
// Simulate error
err := errors.New(errors.CodeNotFound, "resource not found")
// Would write HTTP response
_ = handleError
fmt.Println(errors.GetCode(err))
}
Output: NOT_FOUND
type PlatformError ¶
type PlatformError interface {
error
// Code returns the error code identifying the type of error.
Code() ErrorCode
// Classification returns whether the error is retryable or permanent.
Classification() ErrorClassification
// Message returns the human-readable error message.
Message() string
// Context returns attached metadata as a read-only map.
// Returns nil if no context has been attached.
Context() map[string]interface{}
// Unwrap returns the wrapped error for errors.Is and errors.As compatibility.
// Returns nil if this error does not wrap another error.
Unwrap() error
}
PlatformError extends the standard error interface with structured information for consistent error handling.
PlatformError provides error codes for categorization, classification for retry logic, contextual metadata, and compatibility with standard library error handling (errors.Is, errors.As, errors.Unwrap).
func New ¶
func New(code ErrorCode, message string) PlatformError
New creates a new PlatformError with the given code and message. The error classification is determined by the error code using default mappings.
Example:
err := errors.New(errors.CodeNotFound, "project not found")
Example ¶
package main
import (
"fmt"
"github.com/jmgilman/go/errors"
)
func main() {
err := errors.New(errors.CodeNotFound, "user not found")
fmt.Println(err.Error())
}
Output: [NOT_FOUND] user not found
func Newf ¶
func Newf(code ErrorCode, format string, args ...interface{}) PlatformError
Newf creates a new PlatformError with a formatted message. The error classification is determined by the error code using default mappings.
Example:
err := errors.Newf(errors.CodeInvalidInput, "project name too long: %d characters (max %d)", len(name), maxLen)
Example ¶
package main
import (
"fmt"
"github.com/jmgilman/go/errors"
)
func main() {
userID := "12345"
err := errors.Newf(errors.CodeNotFound, "user %s not found", userID)
fmt.Println(err.Error())
}
Output: [NOT_FOUND] user 12345 not found
func WithClassification ¶
func WithClassification(err error, classification ErrorClassification) PlatformError
WithClassification overrides the classification of an error. Returns a new PlatformError with the specified classification.
This is useful when you need to override the default classification for an error code. For example, marking a normally permanent error as retryable in specific circumstances.
If err is not a PlatformError, it is converted to one with CodeUnknown. Returns nil if err is nil.
Example:
err := errors.New(errors.CodeDatabase, "connection failed") // Normally retryable, but mark as permanent for this case err = errors.WithClassification(err, errors.ClassificationPermanent)
Example ¶
package main
import (
"fmt"
"github.com/jmgilman/go/errors"
)
func main() {
// Override default classification
err := errors.New(errors.CodeDatabase, "database error")
fmt.Println("Default:", errors.IsRetryable(err))
// Mark as permanent for this specific case
err = errors.WithClassification(err, errors.ClassificationPermanent)
fmt.Println("Overridden:", errors.IsRetryable(err))
}
Output: Default: true Overridden: false
func WithContext ¶
func WithContext(err error, key string, value interface{}) PlatformError
WithContext adds a single context field to an error. Returns a new PlatformError with the context field added. Existing context fields are preserved.
If err is not a PlatformError, it is converted to one with CodeUnknown. Returns nil if err is nil.
Example:
err := errors.New(errors.CodeBuildFailed, "build failed") err = errors.WithContext(err, "project", "my-app") err = errors.WithContext(err, "phase", "test")
Example ¶
package main
import (
"fmt"
"github.com/jmgilman/go/errors"
)
func main() {
err := errors.New(errors.CodeBuildFailed, "build failed")
err = errors.WithContext(err, "project", "api")
err = errors.WithContext(err, "phase", "test")
ctx := err.Context()
fmt.Printf("Project: %s, Phase: %s\n", ctx["project"], ctx["phase"])
}
Output: Project: api, Phase: test
func WithContextMap ¶
func WithContextMap(err error, ctx map[string]interface{}) PlatformError
WithContextMap adds multiple context fields to an error. Returns a new PlatformError with the context fields merged. Existing context fields are preserved; new fields override existing ones with the same key.
If err is not a PlatformError, it is converted to one with CodeUnknown. Returns nil if err is nil.
Example:
err := errors.New(errors.CodeExecutionFailed, "execution failed")
err = errors.WithContextMap(err, map[string]interface{}{
"command": "earthly",
"target": "+test",
})
Example ¶
package main
import (
"fmt"
"github.com/jmgilman/go/errors"
)
func main() {
err := errors.New(errors.CodeExecutionFailed, "execution failed")
err = errors.WithContextMap(err, map[string]interface{}{
"command": "earthly",
"target": "+test",
"exit_code": 1,
})
ctx := err.Context()
fmt.Printf("Command: %s, Exit: %d\n", ctx["command"], ctx["exit_code"])
}
Output: Command: earthly, Exit: 1
func Wrap ¶
func Wrap(err error, code ErrorCode, message string) PlatformError
Wrap wraps an error with additional context while preserving the original error. The wrapped error is accessible via Unwrap() and compatible with errors.Is and errors.As.
If the wrapped error is a PlatformError, its classification is preserved. Otherwise, the default classification for the error code is used.
Returns nil if err is nil.
Example:
result, err := repo.Clone(ctx, url)
if err != nil {
return errors.Wrap(err, errors.CodeNetwork, "failed to clone repository")
}
Example ¶
package main
import (
"fmt"
"github.com/jmgilman/go/errors"
)
func main() {
// Simulate database error
dbErr := fmt.Errorf("connection refused")
// Wrap with platform error
err := errors.Wrap(dbErr, errors.CodeDatabase, "failed to connect to database")
fmt.Println(errors.GetCode(err))
}
Output: DATABASE_ERROR
func WrapWithContext ¶
func WrapWithContext(err error, code ErrorCode, message string, ctx map[string]interface{}) PlatformError
WrapWithContext wraps an error and attaches context metadata in a single operation. The context map is copied to prevent external mutation.
Returns nil if err is nil.
Example:
if err := build(ctx); err != nil {
return errors.WrapWithContext(err, errors.CodeBuildFailed, "build failed", map[string]interface{}{
"project": projectName,
"phase": "test",
})
}
func Wrapf ¶
func Wrapf(err error, code ErrorCode, format string, args ...interface{}) PlatformError
Wrapf wraps an error with a formatted message while preserving the original error.
Returns nil if err is nil.
Example:
if err := validate(input); err != nil {
return errors.Wrapf(err, errors.CodeInvalidInput, "validation failed for field %s", fieldName)
}