Adding context to errors in Go
This post compares how to enrich Go errors — both sentinel and typed — with additional context using github.com/ygrebnov/errorc.With
versus the standard fmt.Errorf
approach.
Sentinel error
Let's define a simple sentinel error:
import "errors"
var ErrInvalidInput = errors.New("invalid input")
Using github.com/ygrebnov/errorc.With
function, the context can be added to the sentinel error in the following way:
import "github.com/ygrebnov/errorc"
err := errorc.With(
ErrInvalidInput,
errorc.Field("field1", "value1"),
errorc.Field("field2", "value2"),
)
This approach adds structured key-value context while preserving the original error, enabling accurate identification during error handling.
github.com/ygrebnov/errorc.With
keeps track of the original error. This allows identifying it later during error handling using errors.Is
function:
import (
"errors"
"fmt"
)
if errors.Is(err, ErrInvalidInput) {
fmt.Print("Handled invalid input error: ", err.Error())
}
// Output: Handled invalid input error: invalid input, field1: value1, field2: value2
While fmt.Errorf
is slightly less verbose, it produces a less structured error format:
import "fmt"
err := fmt.Errorf("%w, field1: %s, field2: %s",
ErrInvalidInput,
"value1",
"value2",
)
The same as with github.com/ygrebnov/errorc.With
, the original error is preserved, and it can be checked using errors.Is
.
Typed error
The same as for a sentinel error, the context can be added to a typed error using github.com/ygrebnov/errorc.With
function. The typed error can be later identified using errors.As
function:
package main
import (
"errors"
"fmt"
"github.com/ygrebnov/errorc"
)
type ValidationError struct {
Message string
}
func (e *ValidationError) Error() string {
return e.Message
}
func main() {
// Create a new error of type ValidationError.
err := errorc.With(
&ValidationError{"invalid input"},
errorc.Field("field1", "value1"),
errorc.Field("field2", "value2"),
)
// Identify ValidationError using errors.As.
var ve *ValidationError
if errors.As(err, &ve) {
// Handle ValidationError.
fmt.Print("Handled ValidationError: ", err.Error())
}
// Output: Handled ValidationError: invalid input, field1: value1, field2: value2
}
The same behavior can be achieved using fmt.Errorf
. The error wrapping code is slightly less verbose, but looks less structured:
err := fmt.Errorf("%w, field1: %s, field2: %s",
&ValidationError{"invalid input"},
"value1",
"value2",
)
Performance comparison
The benchmark tests below will compare performance of github.com/ygrebnov/errorc.With
with that of fmt.Errorf
:
import (
"errors"
"fmt"
"testing"
"github.com/ygrebnov/errorc"
)
func BenchmarkWith(b *testing.B) {
baseErr := errorc.New("benchmark error")
field1 := errorc.Field("key1", "value1")
field2 := errorc.Field("key2", "value2")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = errorc.With(baseErr, field1, field2)
}
}
func BenchmarkFmtErrorf(b *testing.B) {
baseErr := errors.New("benchmark error")
val1 := "value1"
val2 := "value2"
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = fmt.Errorf("%w, key1: %s, key2: %s", baseErr, val1, val2)
}
}
These benchmarks measure the cost of constructing wrapped errors with two context fields in a tight loop.
The output of the benchmarks shows that github.com/ygrebnov/errorc.With
is significantly faster than fmt.Errorf
:
BenchmarkWith-8 53965288 21.81 ns/op
BenchmarkFmtErrorf-8 7401583 186.7 ns/op
Conclusion
In conclusion, github.com/ygrebnov/errorc.With
provides a structured and efficient way to add context to errors in Go. Like fmt.Errorf
, it allows for both sentinel and typed errors to be wrapped with additional information while maintaining the original error's identity. Benchmarks show that errorc.With
is significantly faster than fmt.Errorf
, making it a strong candidate for structured and performant error handling in Go applications.