🍃 v0.4.0 Is Here
This release is a massive step forward.
The core pipeline is now up to 5x faster, uses up to 76% less memory, and cuts allocations by up to 98%.
Check out the benchmarks!
We also redesigned the API to make common workflows simpler, added support for nested error trees,
and cleaned up a lot of rough edges.
Enjoy!
About
erax is a Go package that treats errors as first-class data structures rather than plain strings.
It provides error chaining, structured metadata, efficient serialization, and beautiful CLI error traces,
allowing a single error to serve as the source of truth for developers, machines, and observability systems.
Instead of duplicating context across logging, APIs, and tracing, erax keeps semantic information attached to the error itself,
making it easy to render, serialize, inspect, and transport without losing structure.
Example Usage
func CreateUser() error {
err := erax.New("email is already in use")
err = erax.WithMeta(
err,
"failed to create user",
erax.F("code", "503"),
erax.F("info", "This is a really\nreally long information."),
erax.F("user_error", "An account with this email already exists."),
)
return err
}
func Register() error {
err := CreateUser()
return erax.WrapWithErrors(
err,
"failed to register\nbecause of ducks!",
erax.New("random error"),
)
}
func main() {
err := Register()
if err != nil {
erax.Format(err)
}
}
Result:

Result (JSON):
{
"cause": [
{
"message": "random error"
},
{
"cause": {
"message": "email is already in use"
},
"message": "failed to create user",
"meta": {
"code": "503",
"info": "This is a really\nreally long information.",
"user_error": "An account with this email already exists."
}
}
],
"message": "failed to register\nbecause of ducks!"
}
[!Tip]
You might want to check out the full example
🖤 Features
- 🌈 Styled and readable error trace output for CLI
- 🔗 Error chaining
- 🏷️ Attach and retrieve key-value metadata
- 🎨 Configurable colors for trace output
- 🔄 Compatible with standard and third-party errors (e.g., pkg/errors)
- ✨ Fast JSON serialization / deserialization
🔥 Wanna start NOW?
Below is a lame section for those who may feel unconfident.
If you are already cool enough - check out the How To Use guide.
👁 Why?
Most Go applications treat errors as strings.
As an application grows, every layer starts solving the same problem independently:
- logging decides how to serialize an error;
- APIs decide what information should be exposed to clients;
- tracing systems collect additional context;
- developers reconstruct the original failure from logs and stack traces.
The result is that the same information is duplicated across multiple systems, often in different formats.
[!Important]
erax takes a different approach.
An error becomes the single source of truth that carries its own structure, metadata, and context.
The same error can be rendered for a developer, serialized for logging, or inspected by machines
without duplicating information or losing semantics.
For developers
Reading long error chains in logs is frustrating.
Traditional wrapped errors eventually degrade into long text messages where the original failure becomes
difficult to identify.
Once a chain grows large enough, developers usually stop reading it and jump directly to logs or stack traces.
Stack traces help locate where something happened, but they rarely explain why.
[!Important]
erax keeps the structure of the error chain intact and provides a readable error trace instead of
flattening everything into text.
You can:
- print an error directly using
erax.Format();
- deserialize the same error from logs and render exactly the same structured trace.
The result is a representation that is easier to understand than raw logs and significantly more
focused than a stack trace.
For logging
Logging libraries should not define the structure of your errors.
In many applications, every logging package serializes errors differently. Some flatten them into strings,
some inspect them via reflection, while others require custom adapters.
[!Important]
With erax, serialization belongs to the error itself.
Errors can be converted into a compact, deterministic JSON representation that is fast and reflection-free.
Instead of teaching every logging backend how to understand your errors, the error already knows how to represent itself.
Applications constantly need additional information about failures:
- status codes;
- error codes;
- retry hints;
- user-facing messages;
- arbitrary diagnostic context.
Without a common representation, every layer invents its own way to transport this information.
[!Important]
erax keeps metadata attached to the error itself, allowing it to travel through the application together with the failure.
Why not custom error types?
Custom error types are perfectly reasonable. The problem appears when there are hundreds of them.
Large codebases often end up with:
- dozens of packages exporting error types;
- extensive
errors.As() chains;
- tight coupling between unrelated packages;
- APIs that depend on implementation details.
Custom error types are usually written as part of a feature, not as reusable infrastructure.
Their primary goal is to solve today's problem, not to become a stable, high-performance error model for an entire organization.
As a result, every team gradually invents its own conventions for metadata, serialization, wrapping, and interoperability.
Over time, these implementations diverge, are rewritten by new developers, and become increasingly difficult to use consistently across services.
[!Important]
erax keeps the error model consistent while allowing structured metadata without requiring every
failure to become a new Go type.
Why not tracing?
Using tracing as the primary carrier of error context comes at a significant cost.
Every additional attribute, event, or span increases telemetry volume, memory usage, network traffic, and storage costs.
In high-throughput systems, that overhead quickly becomes noticeable.
More importantly, the semantic meaning of an error becomes scattered across tracing infrastructure instead of
traveling with the error itself. Outside the tracing system, much of that context is simply unavailable.
[!Important]
erax keeps error semantics inside the error, where they belong.
Tracing remains responsible for request execution and performance analysis, while errors remain responsible for describing failures.
The two complement each other, but neither should replace the other.
⚡ Why is it Fast?
erax is fast because it aggressively avoids unnecessary allocations, has no hidden overhead, and keeps all hot paths simple:
- Most functions use early returns, so in the common case nothing is allocated or wrapped.
- It relies on pre-sized slices, manual loops, and
strings.Builder with grow hints, instead of reflection or standard library heavy helpers.
- Traversal is done with a small fixed stack instead of recursion, which avoids heap pressure and keeps lookups cheap.
- JSON serialization is optimized with a
sync.Pool buffer reuse, so repeated formatting doesn't constantly allocate new memory.
[!Tip]
Performance comparison summary:
- ▶
erax.Wrap(err, "msg") vs fmt.Errorf("msg: %w", err) vs pkgerrors.Wrap(err, "msg")
- ▶
erax.WithMeta vs otel/trace span.SetAttributes
- ▶ JSON serialisation -
erax vs zap
🔮 Future features (coming soon)
In a short while, you will witness the following things:
- native
[]byte JSON API
GetMetas() []MetaField
- no-color mode
- ASCII branch style
- square branch style
- maybe some performance improvements