Error Interface Cost and Wrapping
Error Interface Cost and Wrapping
Overview
error is an interface — values box dynamic types. Wrapping builds a chain inspected by errors.Is / As. Correctness first; cost matters in ultra-hot paths.
Diagram: Error interface
flow:
[Chain]
|
v
[Is]
Representation
type error interface{ Error() string }
// concrete: *errors.errorString, fmt.wrapError, custom typesReturning error from a function may allocate when constructing fmt.Errorf or custom structs.
Wrapping
return fmt.Errorf("open %s: %w", path, err)
errors.Is(err, fs.ErrNotExist)
errors.As(err, &pathErr)
errors.Join(err1, err2)Performance Notes
| Action | Cost driver |
|---|---|
errors.New sentinel |
One-time |
fmt.Errorf per call |
Alloc + formatting |
Deep Is chains |
Walk + interface asserts |
err.Error() strings |
May alloc; avoid in hot success path |
Pattern: construct rich errors on failure only; keep success path alloc-free.
Sentinel vs Types
var ErrNotFound = errors.New("not found")
type NotFoundError struct{ Key string }
func (e *NotFoundError) Error() string { return "not found: " + e.Key }Experiment
go test -bench=. -benchmempackage errcost_test
import (
"errors"
"fmt"
"testing"
)
var ErrX = errors.New("x")
func BenchmarkSentinel(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = ErrX
}
}
func BenchmarkWrap(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fmt.Errorf("wrap: %w", ErrX)
}
}
func BenchmarkIs(b *testing.B) {
err := fmt.Errorf("wrap: %w", ErrX)
for i := 0; i < b.N; i++ {
_ = errors.Is(err, ErrX)
}
}What to notice: Reusing sentinels is free; wrapping each time allocates.
Try next: Audit a hot path that does fmt.Errorf on success-adjacent branches.