errors

package module
v1.25.0 Latest Latest
Warning

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

Go to latest
Published: May 29, 2025 License: MIT Imports: 8 Imported by: 12

README

errors

Build Status Go Report Card GoDoc Coverage Status

The errors package provides an enterprise-grade error handling approach.

Motivation

1. Unique Error Codes for Client Support

In many corporate applications, it’s not enough for a system to return an error - it must also return a unique, user-facing error code. This code allows end users or support engineers to reference documentation or helpdesk pages that explain what the error means, under what conditions it might occur, and how to resolve it.

These codes serve as stable references and are especially valuable in systems where frontend, backend, and support operations must all stay synchronized on error semantics.

2. Centralized Error Definitions

To make error codes reliable and consistent, they must be centrally defined and maintained in advance. Without such coordination, teams risk introducing duplicate codes, inconsistent messages, or undocumented behaviors. This package was built to support structured error registration and reuse across the entire application.

3. Logging at the Top Level & Contextual Information in Errors

There is an important idiom: log errors only once, and do it as close to the top of the call stack as possible — for instance, in an HTTP controller. Lower layers (business logic, database, etc.) may wrap and propagate errors upward, but only the outermost layer should produce the log entry.

This pattern, while clean and idiomatic, introduces a challenge: how can we include rich, contextual information at the logging point, if it was only known deep inside the application?

To solve this, we need errors that are context-aware. That is, they should carry structured attributes — like the IP address of a failing server, or the input that triggered the issue — as they move up the call stack. This package provides facilities to attach such structured context to errors and extract it later during logging or formatting.

4. Stack Traces for Root Cause Analysis

When diagnosing production issues, developers need more than just error messages — they need stack traces that show where the error originated. This is especially important when multiple wrapping or rethrowing occurs. By capturing the trace at the point of error creation, this package enables faster debugging and clearer logs.

Installation

go get github.com/axkit/errors

Error Template

Predefined errors offer reusable templates for consistent error creation. Use the Template function to declare them:

import "github.com/axkit/errors"

var (
	ErrInvalidInput = errors.Template("invalid input provided").
							Code("CRM-0901").
							StatusCode(400).
							Severity(errors.Tiny)
    
	ErrServiceUnavailable = errors.Template("service unavailable").
							Code("SRV-0253").
							StatusCode(500).
							Severity(errors.Critical)

	// Predefined error gets `Tiny` severity  by default.
	ErrInvalidFilter = errors.Template("invalid filtering rule").
							Code("CRM-0042").
							StatusCode(400)

)

if request.Email == "" {
	return ErrInvalidInput.New().Msg("empty email")
}

if request.Age < 18 {
	return ErrInvalidInput.New().Set("age", request.Age).Msg("invalid age")
}

customer, err := service.CustomerByID(request.CustomerID)
if err != nil {
	return ErrServiceUnavailable.Wrap(err)
}

if customer == nil {
	return ErrInvalidInput.New().Msg("invalid customer")
}

Error Structure

The Error type is the core of this package. It encapsulates metadata, stack traces, and wrapped errors.

Attributes
Attribute Description
message Error message text
severity Severity of the error (Tiny, Medium, Critical)
statusCode HTTP status code
code Application-specific error code
fields Custom key-value pairs for additional context
stack Stack frames showing the call trace

Capturing the Stack Trace

A stack trace is automatically captured at the moment an error is created or first wrapped. This allows developers to identify where the problem originated, even if the error travels up the call stack.

A stack trace is captured when one of the following methods is called:

  • errors.TemplateError.Wrap(...)
  • errors.TemplateError.New(...)
  • errors.Wrap(...)

Rewrapping an error does not overwrite an existing stack trace. The original call site remains preserved, ensuring consistent and reliable debugging information.

Error Logging

Effective error logging is crucial for debugging and monitoring. This package encourages logging errors at the topmost layer of the application, such as an HTTP controller, while lower layers propagate errors with additional context. This ensures that logs are concise and meaningful.

var ErrInvalidObjectID = errors.Template("inalid object id").Code("CRM-0400").StatusCode(400)

// customer_repo.go
customerTable := velum.NewTable[Customer]("customers")

customer, err := customerTable.GetByPK(ctx, db, customerID)
return customer, err

// customer_service.go
customer, err := repo.CustomerByID(customerID)
if err != nil && errors.Is(err, repo.ErrNotFound) {
    return nil, ErrInvalidObjectID.Wrap(err).Set("customerId", customerID)
}

// customer_controller.go
customer, err := service.CustomerByID(customerID)
if err != nil {
	buf := errors.ToJSON(err, errors.WithAttributes(errors.AddStack|errors.AddWrappedErrors))
	
	// server output (extended)
	log.Println(string(buf))
	
	// client output (reduced)
	buf = errors.ToJSON(err)
}
Custom JSON Serialization

If you need to implement a custom JSON serializer, the errors.Serialize(err) method provides an object containing all public attributes of the error. This allows you to define your own serialization logic tailored to your application's requirements.

Alarm Notifications

Set an alarmer to notify on critical errors:

type CustomAlarmer struct{}

func (c *CustomAlarmer) Alarm(se *SerializedError) {
    fmt.Println("Critical error:", err)
}

errors.SetAlarmer(&CustomAlarmer{})

var ErrConsistencyFailed = errors.Template("data consistency failed").Severity(errors.Critical) 

// CustomAlarmer.Alarm() will be invocated automatically (severity=Critical)
return ErrDataseConnectionFailure.New()

Severity Levels

The package classifies errors into three severity levels:

  • Tiny: Minor issues, typically validation errors.
  • Medium: Regular errors that log stack traces.
  • Critical: Major issues requiring immediate attention.

When wrapping errors, the severity and statusCode attributes can be overridden. The client will always receive the latest severity and statusCode values from the outermost error. Any inner errors even with higher severity or different status codes will only be logged, ensuring that the most relevant information is presented to the client while maintaining detailed logs for debugging purposes.

Migration Guide

Below is a categorized list of how errors are typically created or obtained in Go code. These represent common entry points for error handling.


// 1. plain, unstructured error
return errors.New("message") 

// 2. formatted, unstructured 
return fmt.Errorf("something failed") 

// 3. wrapping + formatting
return fmt.Errorf("wrap: %w", err) 

// 4. Custom struct implementing error
return &CustomError{}

// 5. External packages returning error values
rows, err := db.Query("SELECT COUNT(1) FROM customers")
if err != nil {
	return nil, err 
}

// 6. shared "constants"
var ErrX = errors.New("...")

// 7. Sentinel errors for comparison via errors.Is(...)
// 8. Errors returned from standard library or external packages(e.g., io.EOF, pgx.ErrNoRows) 
Migration Strategy

Migrating to axkit/errors can be done incrementally, without needing to rewrite your entire codebase at once. The primary goal is to transition from unstructured error handling to a system of reusable, structured templates with support for metadata, stack traces, and observability.

Step 1: Replace the Import

Start by replacing the standard library import:

import "errors"

with:

import "github.com/axkit/errors"

This change is non-breaking: errors.New(...) remains available and behaves the same way, returning a basic error without stack trace. This allows your application to compile and function as before.

Step 2: Identify and Replace Static Errors with Templates

Begin replacing predefined or shared errors like:

var ErrUnauthorized = errors.New("unauthorized")

with structured templates:

var ErrUnauthorized = errors.Template("unauthorized").
	Code("AAA-0401").
	StatusCode(401).
	Severity(Tiny)

I recommend placing templates at the top of each file or organizing them into a dedicated file such as errors.go, error_templates.go, etc.

Step 3: Wrap External or Lower-Level Errors

Whenever you receive an error from the standard library or a third-party package (e.g. pgx.ErrNoRows, io.EOF, sql.ErrTxDone), wrap it in your own context using a template:

customers, err := s.repo.CustomerByID(customerID)
if err == pgx.ErrNoRows {
	return ErrCustomerNotFound.Wrap(err)
}

If no reusable template exists yet, it’s acceptable to inline one during early migration:

customers, err := s.repo.CustomerByID(customerID)
if err == pgx.ErrNoRows {
	return errors.Wrap(err, "customer not found").StatusCode(400).Set("customerId", customerID)
}
Step 4: Centralize and Document Templates

As migration progresses, ensure that all templates:

  • Have a unique Code(...) identifier
  • Are grouped and reusable
  • Are linked to documentation or support systems (e.g. HelpDesk, monitoring, alerting)
  • Capture stack trace at the appropriate level via .New() or .Wrap()

This ensures a consistent and observable error-handling experience across your application.

Summary
Step Goal Complexity Backward Compatible
Step 1 Replace standard import Very Low ✅ Yes
Step 2 Use Template for shared errors Medium ✅ Yes
Step 3 Wrap external or third-party errors Medium ✅ Yes
Step 4 Centralize and document templates High (but worth it) ✅ Yes

License

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

Documentation

Overview

Package errors provides a structured and extensible way to create, wrap, and manage errors in Go applications. It includes support for adding contextual information, managing error hierarchies, and setting attributes such as severity, HTTP status codes, and custom error codes.

The package is designed to enhance error handling by allowing developers to attach additional metadata to errors, wrap underlying errors with more context, and facilitate debugging and logging. It also supports integration with alerting systems through the Alarm method.

Key features include: - Wrapping errors with additional context. - Setting custom attributes like severity, status codes, and business codes. - Managing error stacks and hierarchies. - Sending alerts for critical errors. - Support for custom key-value pairs to enrich error information. - Integration with predefined error types for common scenarios. - Serialization errors for easy logging.

Index

Examples

Constants

View Source
const (
	ServerOutputFormat      = AddProtected | AddStack | AddFields | AddWrappedErrors
	ServerDebugOutputFormat = AddProtected | AddStack | AddFields | AddWrappedErrors | IndentJSON
	ClientDebugOutputFormat = AddProtected | AddStack | AddFields | AddWrappedErrors
	ClientOutputFormat      = 0 // no fields, no stack, no wrapped errors, only message.
)

Variables

View Source
var (
	// CallerFramesFunc holds default function used by function Catch()
	// to collect call frames.
	CallerFramesFunc func(offset int) []StackFrame = DefaultCallerFrames

	// CallingStackMaxLen holds maximum elements in the call frames.
	CallingStackMaxLen int = 15
)
View Source
var ErrMarshalError = Template("error marshaling failed").Severity(Critical).StatusCode(500)

Functions

func As added in v0.0.2

func As(err error, target any) bool

As checks if the error can be cast to a target type.

func Is added in v0.0.2

func Is(err error, target error) bool

Is checks if the error is of the same type as the target error.

func New

func New(msg string) error

New creates and returns a standard Go error using the built-in errors.New function.

This is implemented to maintain compatibility with existing Go error handling practices. However, the design philosophy of this package does not encourage the use of errors.New(msg) as commonly practiced in Go. Instead, it promotes the use of structured and enriched error handling mechanisms provided by this package.

func SetAlarmer added in v1.0.0

func SetAlarmer(a Alarmer)

SetAlarmer sets Alarmer implementation to be used when critical error is caught.

func ToJSON added in v0.0.2

func ToJSON(err error, opts ...Option) []byte

ToJSON serializes the error to JSON format.

Example

ExampleToJSON demonstrates generating JSON output for an error.

package main

import (
	"fmt"

	"github.com/axkit/errors"
)

func main() {
	jsonErr := errors.Template("User not found").Code("E404").StatusCode(404).Severity(errors.Tiny)
	jsonOutput := errors.ToJSON(jsonErr, errors.WithAttributes(errors.AddFields))
	fmt.Println("JSON Error:", string(jsonOutput))
}
Output:

JSON Error: {"msg":"User not found","severity":"tiny","code":"E404","statusCode":404}

Types

type Alarmer added in v0.0.2

type Alarmer interface {
	Alarm(err error)
}

Alarmer is an interface wrapping a single method Alarm

Alarm is invocated automatically when critical error is caught, if alarmer is set.

Example
package main

import (
	"fmt"

	"github.com/axkit/errors"
)

type CustomAlarmer struct{}

func (c *CustomAlarmer) Alarm(err error) {
	fmt.Println("Critical error:", err)
}

func main() {

	errors.SetAlarmer(&CustomAlarmer{})
	var ErrSystemFailure = errors.Template("system failure").Severity(errors.Critical)

	ErrSystemFailure.New().Set("path", "/var/lib").Alarm()

}
Output:

Critical error: system failure

type Error added in v1.0.0

type Error struct {
	// contains filtered or unexported fields
}

Error represents a structured error with metadata, custom fields, stack trace, and optional wrapping.

func Wrap added in v0.0.6

func Wrap(err error, message string) *Error

Wrap wraps an existing error with a new message, effectively creating a new error that includes the previous error.

Example

ExampleWrap demonstrates wrapping an error.

package main

import (
	"fmt"

	"github.com/axkit/errors"
)

func main() {
	innerErr := errors.Template("Database connection failed")
	outerErr := errors.Template("Service initialization failed").Wrap(innerErr)
	fmt.Println("Wrapped Error:", outerErr.Error())
}
Output:

Wrapped Error: Service initialization failed: Database connection failed

func (*Error) Alarm added in v1.0.0

func (e *Error) Alarm()

Alarm triggers an alert for the error if an alarmer is configured.

func (*Error) Code added in v1.0.0

func (e *Error) Code(code string) *Error

Code sets a custom application-specific code for the error.

func (*Error) Error added in v1.0.0

func (e *Error) Error() string

Error returns the error message, including any wrapped error messages.

Example
package main

import (
	"fmt"

	"github.com/axkit/errors"
)

func main() {

	type Input struct {
		ID        int    `json:"id"`
		FirstName string `json:"firstName"`
		LastName  string `json:"lastName"`
	}

	var ErrEmptyAttribute = errors.Template("empty attribute value").Code("CMN-0400")
	var ErrInvalidInput = errors.Template("invalid input").Code("CMN-0400")

	validateInput := func(inp *Input) error {
		if inp.ID == 0 {
			return ErrEmptyAttribute.New().Set("emptyFields", []string{"id"})
		}
		return nil
	}

	if err := validateInput(&Input{}); err != nil {
		returnErr := ErrInvalidInput.Wrap(err)
		fmt.Println(returnErr.Error())

	}
}
Output:

invalid input: empty attribute value

func (*Error) Msg added in v1.0.0

func (e *Error) Msg(s string) *Error

Msg sets the error message and marks the error as not being a pure wrapper.

func (*Error) Protected added in v1.0.0

func (e *Error) Protected(protected bool) *Error

Protected marks the error as protected to prevent certain modifications or exposure.

func (*Error) Set added in v1.0.0

func (e *Error) Set(key string, value any) *Error

Set adds or updates a custom key-value pair in the error's fields.

func (*Error) Severity added in v1.0.0

func (e *Error) Severity(severity SeverityLevel) *Error

Severity sets the severity level for the error.

func (*Error) StatusCode added in v1.0.0

func (e *Error) StatusCode(statusCode int) *Error

StatusCode sets the associated HTTP status code for the error.

func (*Error) Wrap added in v1.0.0

func (e *Error) Wrap(err error) *Error

Wrap returns a new Error that wraps the given error while retaining the current error's metadata and fields. It also preserves the stack trace of the wrapped error if available. The new error is marked as a pure wrapper if the original error is of type ErrorTemplate or Error. If the original error is nil, it returns the current error. This method is useful for chaining errors and maintaining context. It supports wrapping both ErrorTemplate and Error types, preserving their fields and stack trace.

func (*Error) WrappedErrors added in v1.0.0

func (err *Error) WrappedErrors() []Error

WrappedErrors returns a slice of all wrapped errors, including the current one if it's not a pure wrapper.

type ErrorFormattingOptions added in v1.0.0

type ErrorFormattingOptions struct {
	// contains filtered or unexported fields
}

type ErrorSerializationRule added in v1.0.0

type ErrorSerializationRule uint8
const (

	// AddStack - add stack in the JSON.
	AddStack ErrorSerializationRule = 1 << iota

	// AddProtected - add protected errors in the JSON.
	AddProtected

	// AddFields - add fields in the JSON.
	AddFields

	// AddWrappedErrors - add previous errors in the JSON.
	AddWrappedErrors

	IndentJSON
)

type ErrorTemplate added in v1.0.0

type ErrorTemplate struct {
	// contains filtered or unexported fields
}

ErrorTemplate defines a reusable error blueprint that includes metadata and custom key-value fields. It is designed for creating structured errors with consistent attributes such as severity and HTTP status code.

Example

ExamplePredefinedErrors demonstrates using predefined errors.

package main

import (
	"fmt"

	"github.com/axkit/errors"
)

func main() {
	var ErrDatabaseDown = errors.Template("Database is unreachable").
		Code("DB-500").
		StatusCode(500).
		Severity(errors.Critical)

	if err := openDatabase("pg:5432"); err != nil {
		fmt.Println("Error:", ErrDatabaseDown.Wrap(err).Error())

	}
}

func openDatabase(connStr string) error {
	var dbErr error
	return errors.Wrap(dbErr, "unable to connect to database").Set("connectionString", connStr)
}
Output:

Error: Database is unreachable: unable to connect to database

func Template added in v1.0.0

func Template(msg string) *ErrorTemplate

Template returns a new ErrorTemplate initialized with the given message. It can be extended with additional attributes and reused to create multiple error instances.

func (*ErrorTemplate) Code added in v1.0.0

func (et *ErrorTemplate) Code(code string) *ErrorTemplate

Code sets an application-specific error code on the template.

func (*ErrorTemplate) Error added in v1.0.0

func (et *ErrorTemplate) Error() string

Error returns the error message from the template.

func (*ErrorTemplate) New added in v1.0.0

func (et *ErrorTemplate) New() *Error

New creates a new Error instance using the template's metadata and fields. A new stack trace is captured at the point of the call.

func (*ErrorTemplate) Protected added in v1.0.0

func (et *ErrorTemplate) Protected(protected bool) *ErrorTemplate

Protected marks the error as protected, indicating it should not be exposed externally.

func (*ErrorTemplate) Set added in v1.0.0

func (et *ErrorTemplate) Set(key string, value any) *ErrorTemplate

Set adds a custom key-value pair to the template's fields.

func (*ErrorTemplate) Severity added in v1.0.0

func (et *ErrorTemplate) Severity(severity SeverityLevel) *ErrorTemplate

Severity sets the severity level for the error template.

func (*ErrorTemplate) StatusCode added in v1.0.0

func (et *ErrorTemplate) StatusCode(statusCode int) *ErrorTemplate

StatusCode sets the HTTP status code associated with the error.

func (*ErrorTemplate) Wrap added in v1.0.0

func (et *ErrorTemplate) Wrap(err error) *Error

Wrap wraps an existing error with the ErrorTemplate's metadata and fields. It supports wrapping both ErrorTemplate and Error types, preserving their fields and stack trace.

type Option added in v1.0.0

type Option func(*ErrorFormattingOptions)

func WithAttributes added in v1.0.0

func WithAttributes(rule ErrorSerializationRule) Option

func WithRootLevelFields added in v1.0.0

func WithRootLevelFields(fields []string) Option

func WithStopStackOn added in v1.0.0

func WithStopStackOn(stopOnFuncContains string) Option

WithStopStackOn sets the function name to stop the adding stack frames. As instance: WithStopStackOn("fasthttp") will stop adding stack frames when the function name contains "fasthttp". It's useful to avoid adding stack frames of the libraries which are not interesting for the user.

type SerializedError added in v1.0.0

type SerializedError struct {
	Message    string            `json:"msg"`
	Severity   string            `json:"severity,omitempty"`
	Code       string            `json:"code,omitempty"`
	StatusCode int               `json:"statusCode,omitempty"`
	Fields     map[string]any    `json:"fields,omitempty"`
	Wrapped    []SerializedError `json:"wrapped,omitempty"`
	Stack      []StackFrame      `json:"stack,omitempty"`
}

SerializedError is serialization ready error.

func Serialize added in v1.0.0

func Serialize(err error, opts ...Option) *SerializedError

Serialize serializes the error to a SerializedError struct.

type SeverityLevel

type SeverityLevel int

SeverityLevel describes error severity levels.

const (
	Unknown SeverityLevel = iota
	// Tiny classifies expected, managed errors that do not require administrator attention.
	// Writing a call stack to the journal file is not recommended.
	//
	// Example: error related to validation of entered form fields.
	Tiny

	// Medium classifies a regular error. A call stack is written to the log.
	Medium

	// Critical classifies a significant error, requiring immediate attention.
	// The occurrence of the error should be communicated to the administrator
	// through all available channels. A call stack is written to the log.
	// If Alarmer is set, it will be called.
	Critical
)

func (SeverityLevel) MarshalJSON

func (sl SeverityLevel) MarshalJSON() ([]byte, error)

MarshalJSON implements json/Marshaller interface.

func (SeverityLevel) String

func (sl SeverityLevel) String() string

String returns severity level string representation.

func (*SeverityLevel) UnmarshalJSON added in v1.0.0

func (sl *SeverityLevel) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json/Unmarshaller interface.

type StackFrame added in v1.0.0

type StackFrame struct {
	Function string `json:"func"`
	File     string `json:"file"`
	Line     int    `json:"line"`
}

StackFrame describes content of a single stack frame stored with error.

func DefaultCallerFrames added in v0.0.2

func DefaultCallerFrames(offset int) []StackFrame

DefaultCallerFrames returns default implementation of call frames collector.

func (StackFrame) String added in v1.0.0

func (s StackFrame) String() string

Jump to

Keyboard shortcuts

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