The errors package provides an enterprise-grade error handling approach.
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.
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.
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.
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.
go get github.com/axkit/errors
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")
}
The Error
type is the core of this package. It encapsulates metadata, stack traces, and wrapped errors.
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 |
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.
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)
}
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.
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()
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.
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)
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.
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.
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.
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)
}
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.
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 |
This project is licensed under the MIT License. See the LICENSE
file for details.