- Overview
- Performance
- Features
- Installation
- Usage
- Templates
- Generator Usage
- API Reference
- Drop-in Replacement Compatibility
- API Stability
- Examples
- Contributing
- License
- Acknowledgments
This ready-to-use library provides structured error handling for Go applications, designed as a complete drop-in
replacement for the standard errors package. It extends standard error functionality with:
- Structured attributes for rich error context
- Error wrapping and joining with full compatibility with
errors.Is,errors.As, anderrors.Join - Stack trace capture for debugging
- JSON serialization for structured logging
- Direct integration with popular logging frameworks (Zap, Zerolog, Logrus, slog)
- Customizable code generation for your own logging frameworks
This library is designed with performance as a core principle. The primary goal is to provide meaningful, structured error messages with maximum performance by:
- Avoiding reflection wherever possible - type-safe attribute helpers eliminate runtime reflection overhead
- Zero-allocation string building for common error formatting paths
- Efficient marshaling - custom marshalers for each logging framework avoid generic reflection-based serialization
- Lazy evaluation - error details are only formatted when actually logged or serialized
- Minimal allocations - careful memory management to reduce GC pressure
The library maintains full compatibility with Go's standard errors package while adding powerful structured error
handling capabilities:
import errors "github.com/emiliogrv/errors/pkg/full"
err := errors.New("database connection failed").
WithAttrs(
errors.String("host", "localhost"),
errors.Int("port", 5432),
errors.Duration("timeout", 30*time.Second),
).
WithTags("database", "critical").
WithStack(debug.Stack())Import only what you need! To avoid pulling all logging framework dependencies into your go.mod, you can import
specific logger packages:
// Import only Zap support
import errors "github.com/emiliogrv/errors/pkg/zap"
// Import only slog support
import errors "github.com/emiliogrv/errors/pkg/slog"
// Import only Logrus support
import errors "github.com/emiliogrv/errors/pkg/logrus"
// Import only Zerolog support
import errors "github.com/emiliogrv/errors/pkg/zerolog"
// Import only core functionality (no logger integrations)
import errors "github.com/emiliogrv/errors/pkg/core"Each partial import includes the core templates plus the specific logger integration, keeping your dependencies minimal.
The library supports custom template generation, allowing you to:
- Generate your own logger integrations by providing custom templates
- Override default templates to customize behavior
- Add new functionality by simply using the
-input-dirflag with your template directory
Example:
go run github.com/emiliogrv/errors/cmd/errors_generator \
-input-dir ./my-templates \
-output-dir ./generated \
-formats mylogger,zapCore templates are always generated regardless of which formats you specify, ensuring base functionality is always available.
go get github.com/emiliogrv/errorsimport errors "github.com/emiliogrv/errors/pkg/full"
// Simple error
err := errors.New("something went wrong")
// Error with attributes
err := errors.New("failed to process request").
WithAttrs(
errors.String("user_id", "12345"),
errors.Int("retry_count", 3),
errors.Bool("recoverable", true),
)// Wrap errors with context
if err := doSomething(); err != nil {
return errors.New("operation failed").
WithErrors(err).
WithAttrs(errors.String("operation", "doSomething"))
}
// Join multiple errors
err := errors.Join(
errors.New("validation failed"),
errors.New("missing required field: email"),
errors.New("missing required field: name"),
)import (
errors "github.com/emiliogrv/errors/pkg/full"
"go.uber.org/zap"
)
func main() {
err := errors.New("operation failed").
WithAttrs(errors.Int("code", 500))
// Zap
logger, _ := zap.NewProduction()
logger.Error("error occurred", zap.Any("err", err))
// JSON marshaling
jsonData, _ := json.Marshal(err)
// Logrus
logrus.WithFields(logrus.Fields{
"err": err.(*errors.StructuredError).MarshalLogrusFields(),
}).
Error("error occurred")
}import (
errors "github.com/emiliogrv/errors/pkg/zap" // Only Zap dependency
"go.uber.org/zap"
)
func main() {
err := errors.New("operation failed").
WithAttrs(errors.Int("code", 500))
logger, _ := zap.NewProduction()
logger.Error("error occurred", zap.Any("err", err))
}Create your custom template (e.g., tmpl/mylogger.tmpl):
// Code generated by errors_generator; DO NOT EDIT.
// Generated at {{ .Date }}
// Version {{ .Version }}
package {{ .PackageName }}
// MarshalMyLogger is the implementation for MyLogger.
func (receiver *StructuredError) MarshalMyLogger() error {
// Your custom marshaling logic here
return nil
}Generate code:
go run github.com/emiliogrv/errors/cmd/errors_generator \
-input-dir ./tmpl \
-output-dir ./generated \
-formats myloggerCore templates are always generated and provide the fundamental error handling functionality:
| Template | Description |
|---|---|
attr.go |
Type-safe attribute helpers (String, Int, Bool, Time, Duration, etc.) |
common.go |
Common utilities and depth control for marshaling |
error.go |
Core StructuredError type and basic methods |
join.go |
Join and JoinIf functions for combining errors |
json.go |
JSON marshaling/unmarshaling support |
map.go |
Map representation for generic structured output |
string.go |
String formatting and Error() method implementation |
wrap.go |
Unwrap, Is, and As methods for error wrapping |
Additional templates for specific logging framework integrations:
| Package | Templates | Dependencies |
|---|---|---|
pkg/full |
Core + Zap + Zerolog + Logrus + slog | All logger dependencies |
pkg/zap |
Core + Zap | go.uber.org/zap |
pkg/zerolog |
Core + Zerolog | github.com/rs/zerolog |
pkg/logrus |
Core + Logrus | github.com/sirupsen/logrus |
pkg/slog |
Core + slog | Standard library only |
pkg/core |
Core only | No external dependencies |
You can override any default template by providing a template with the same name in your input directory:
# Override the default zap.tmpl with your custom version
go run github.com/emiliogrv/errors/cmd/errors_generator \
-input-dir ./my-templates \
-output-dir ./generated \
-formats zapIf my-templates/zap.tmpl exists, it will replace the built-in Zap template.
The code generator supports various options:
go run github.com/emiliogrv/errors/cmd/errors_generator [options]
Options:
-formats string
Comma-separated list of formats to generate, or 'all' to generate all formats (default: core)
-help
Show this help message
-input-dir string
Path to user templates directory (optional)
-output-dir string
Output directory for generated files
-export-dir string
Export default templates to the specified directory and exit
-package string
Package name for generated code (default: errors) (default "errors")
-test-gen string
Test generation level: none, flex, strict (default: none) (default "none")
-with-gen-header
Include generated message in generated code (default: true) (default true)# Export default templates for customization
go run github.com/emiliogrv/errors/cmd/errors_generator -export-dir ./my-templates
# Generate core templates only
go run github.com/emiliogrv/errors/cmd/errors_generator -output-dir ./pkg/core
# Generate with Zap support
go run github.com/emiliogrv/errors/cmd/errors_generator \
-output-dir ./pkg/zap \
-formats zap
# Generate all available formats
go run github.com/emiliogrv/errors/cmd/errors_generator \
-output-dir ./pkg/full \
-formats all
# Generate with custom templates
go run github.com/emiliogrv/errors/cmd/errors_generator \
-input-dir ./my-templates \
-output-dir ./generated \
-formats mylogger,zap
# Generate with tests
go run github.com/emiliogrv/errors/cmd/errors_generator \
-output-dir ./pkg/full \
-formats all \
-test-gen stricttype StructuredError struct {
Message string // Primary error message
Attrs []Attr // Structured attributes
Errors []error // Wrapped errors
Tags []string // Categorical labels
Stack []byte // Stack trace (optional)
}type Attr struct {
Key string
Value any
Type Type
}New(message string) *StructuredError- Create a new structured errorJoin(errs ...error) error- Join multiple errors (nil-safe)JoinIf(errs ...error) error- Join errors only if first is non-nilIs(err, target error) bool- Check error equality (alias toerrors.Is)As(err error, target any) bool- Type assertion (alias toerrors.As)Unwrap(err error) error- Unwrap single error (alias toerrors.Unwrap)
Type-safe helpers for common types:
String(key, value string) AttrInt(key string, value int) AttrInt64(key string, value int64) AttrUint64(key string, value uint64) AttrFloat64(key string, value float64) AttrBool(key string, value bool) AttrTime(key string, value time.Time) AttrDuration(key string, value time.Duration) AttrAny(key string, value any) AttrObject(key string, attrs ...Attr) Attr
Each helper also has a plural version (e.g., Ints, Strings, Bools) for slices.
WithAttrs(attrs ...Attr) *StructuredError- Add attributesWithErrors(errors ...error) *StructuredError- Set wrapped errorsWithTags(tags ...string) *StructuredError- Add tagsWithStack(stack []byte) *StructuredError- Set stack tracePrependErrors(errors ...error) *StructuredError- Add errors at the beginningAppendErrors(errors ...error) *StructuredError- Add errors at the endError() string- Implement error interfaceUnwrap() []error- Implement multi-unwrapper interfaceMarshalJSON() ([]byte, error)- JSON marshalingUnmarshalJSON(data []byte) error- JSON unmarshaling
// Set maximum depth for error marshaling (default: 100)
errors.SetMaxDepthMarshal(depth int)
// Get current maximum depth
errors.MaxDepthMarshal() intThis library is designed as a complete drop-in replacement for Go's standard errors package. You can replace:
import "errors"with:
import errors "github.com/emiliogrv/errors/pkg/full"And your existing code will continue to work as expected.
While this library maintains full API compatibility with the standard errors package, there are some behavioral
differences to be aware of:
When using fmt.Errorf to wrap a StructuredError, the text is preserved in the fmt.Errorf wrapper. However, if
that wrapped error is then added to another StructuredError via WithErrors() or Join(), the fmt.Errorf wrapper
text is lost because the marshaling extracts the inner StructuredError directly:
base := errors.New("base error")
wrapped := fmt.Errorf("text example %w", base)
// Text is preserved when calling wrapped.Error() directly:
fmt.Println(wrapped.Error()) // Output: "text example (message=base error)"
// But text is lost when nested in another StructuredError:
nested := errors.New("outer").WithErrors(wrapped)
fmt.Println(nested.Error())
// Output: (message=outer),
// (errors=[
// (message=base error)
// ])
// The "text example" part is lostWorkaround: Use structured error capabilities directly instead of mixing fmt.Errorf with WithErrors():
base := errors.New("base error")
wrapped := errors.New("text example").WithErrors(base)
// Now "text example" is preserved in the error chainThe string representation of errors created with this package may differ from standard errors when using structured features:
- Standard
errors.New(): Produces identical output toerrors.New()from the standard library - With structured features: Using
WithErrors(),WithAttrs(), etc. produces a different format that includes the structured information
// These produce identical string output:
stdErr := stderrors.New("error message")
customErr := errors.New("error message")
// These produce different string output:
stdWrapped := fmt.Errorf("wrapper: %w", stderrors.New("base"))
customWrapped := errors.New("wrapper").WithErrors(errors.New("base"))This package implements the multi-error unwrapper interface (Unwrap() []error), while fmt.Errorf with %w
implements the single-error unwrapper interface (Unwrap() error). The key difference is in how StructuredError
serializes wrapped errors:
-
fmt.Errorf("text %w", err)on its own:- Preserves all text when calling
.Error() - Example:
fmt.Errorf("text example %w", err).Error()shows"text example: <err message>" - Implements single-error unwrapper interface
- Preserves all text when calling
-
fmt.Errorfwrapped inStructuredError:- When
StructuredErrorserializes the error, it callsUnwrap()to extract the inner error fmt.Errorfignores the wrapping text when unwrapped, returning only the inner error- Result: The wrapping text is lost during
StructuredErrorserialization - Example:
errors.New("outer").WithErrors(fmt.Errorf("text example %w", err))loses "text example"
- When
-
WithErrors()wrapped errors:- Preserves all text and structure during serialization
- Does not lose context when unwrapped by another
StructuredError - Implements the multi-unwrap interface for error chaining
- Maintains full context in both the error chain and string output
For example:
// fmt.Errorf preserves text on its own
wrapped1 := fmt.Errorf("text example %w", errors.New("base error"))
fmt.Println(wrapped1.Error()) // => "text example: base error" ✓
// But text is lost when StructuredError unwraps it
wrapped2 := errors.New("outer").WithErrors(wrapped1)
fmt.Println(wrapped2.Error()) // => "(message=outer), (errors=[(message=base error)])" - loses "text example" ✗
// WithErrors preserves all text
wrapped3 := errors.New("wrapper").WithErrors(errors.New("base error"))
fmt.Println(wrapped3.Error()) // => "(message=wrapper), (errors=[(message=base error)])" ✓The library includes comprehensive compatibility tests to ensure drop-in replacement behavior. See
pkg/full/compatibility_test.go for detailed test cases covering:
- String output comparison between standard and structured errors
errors.Is()behavior with both error typeserrors.As()behavior for extractingStructuredErrorerrors.Unwrap()behavior with single and multi-unwrap interfaceserrors.Join()compatibility- Nil error handling
fmt.Errorfbehavior with%wverb- The known text loss when nesting
fmt.Errorfwrapped errors inWithErrors()
Once version 1.0 is released, the API will follow semantic versioning strictly.
Complete examples are available in the examples/ directory:
- full import - Full import with all logger integrations
- partial import - Partial import (Zap only)
- custom tmpl - Custom template generation
Contributions are welcome! Please feel free to submit issues or pull requests.
This project is licensed under the BSD 3-Clause License - see the LICENSE file for details.
This library is designed to be a drop-in replacement for Go's standard errors package while providing enhanced
functionality for modern application development.