All Benchmarks

Go Benchmark

Error Handling Cost

What creating, wrapping, and inspecting errors really costs.

errorserror-wrappingerrors-iserrors-asfmt-errorfallocation

Almost every Go function returns an error, and most codebases wrap them on the way up with fmt.Errorf("...: %w", err) — but few people know what that convention costs. This benchmark measures the full error lifecycle: creating an error with errors.New, formatting one with fmt.Errorf, wrapping with the %w verb, and checking errors via a direct sentinel comparison, errors.Is, and errors.As. The takeaway: creating and formatting errors allocates on every call, while checking them is nearly free — so the cost lives on the error-producing path, not the error-checking path.

linux/amd64AMD Ryzen 9 9950X3D 16-Core Processorbenchmarks/error-handling-cost
Compare atCPUs

1 CPU

Fastest

Sentinel Check

1.07 ns/op

Slowest

Fmt Errorf Wrap

89.71 ns/op · 84x slower

32 CPUs

Fastest

Sentinel Check

1.11 ns/op

Slowest

Fmt Errorf Wrap

90.35 ns/op · 81x slower

Performance Comparison (lower is better)
CPU:
#1

Sentinel Check #

Fastest

Compares an existing error against a package-level sentinel with err == errNotFound. A plain interface comparison: no allocation, no unwrapping, effectively free — but it breaks as soon as anyone wraps the error in between.

CPU Scaling (lower is better)
func BenchmarkSentinelCheck_run(b *testing.B) {
	errNotFound := errors.New("user not found")
	errs := []error{errNotFound, errors.New("permission denied")}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		sinkBool = errs[i%len(errs)] == errNotFound
	}
}
1 CPU
46.9xfaster(4587%)thanErrors As
8.1xfaster(708%)thanErrors Is
10.9xfaster(995%)thanErrors New
68.5xfaster(6747%)thanFmt Errorf
83.8xfaster(8278%)thanFmt Errorf Wrap
32 CPUs
44.2xfaster(4319%)thanErrors As
6.1xfaster(512%)thanErrors Is
10xfaster(902%)thanErrors New
73.2xfaster(7225%)thanFmt Errorf
81.4xfaster(8038%)thanFmt Errorf Wrap
#2

Errors Is #

Calls errors.Is(err, errNotFound) on a two-level wrapped chain. It walks the chain via Unwrap() and compares each link against the target. Slower than a raw ==, but it is the only comparison that survives wrapping.

CPU Scaling (lower is better)
func BenchmarkErrorsIs_run(b *testing.B) {
	errNotFound := errors.New("user not found")
	// A two-level wrapped chain, as produced by typical layered code.
	err := fmt.Errorf("handling request: %w", fmt.Errorf("loading user: %w", errNotFound))
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		sinkBool = errors.Is(err, errNotFound)
	}
}
1 CPU
5.8xfaster(480%)thanErrors As
1.4xfaster(35%)thanErrors New
8.5xfaster(747%)thanFmt Errorf
10.4xfaster(937%)thanFmt Errorf Wrap
8.1xslower(708%)thanSentinel Check
32 CPUs
7.2xfaster(622%)thanErrors As
1.6xfaster(64%)thanErrors New
12xfaster(1097%)thanFmt Errorf
13.3xfaster(1230%)thanFmt Errorf Wrap
6.1xslower(512%)thanSentinel Check
#3

Errors New #

Creates a fresh error with errors.New("user not found") on every iteration. Each call allocates a new errorString value on the heap. This is why sentinel errors are declared once at package level instead of being recreated per call.

CPU Scaling (lower is better)
func BenchmarkErrorsNew_run(b *testing.B) {
	for i := 0; i < b.N; i++ {
		sinkErr = errors.New("user not found")
	}
}
1 CPU
4.3xfaster(328%)thanErrors As
1.4xslower(35%)thanErrors Is
6.3xfaster(525%)thanFmt Errorf
7.7xfaster(665%)thanFmt Errorf Wrap
10.9xslower(995%)thanSentinel Check
32 CPUs
4.4xfaster(341%)thanErrors As
1.6xslower(64%)thanErrors Is
7.3xfaster(631%)thanFmt Errorf
8.1xfaster(712%)thanFmt Errorf Wrap
10xslower(902%)thanSentinel Check
#4

Errors As #

Calls errors.As(err, &target) on a two-level wrapped chain to extract a custom *NotFoundError. Like errors.Is it walks the chain, but additionally performs a type check per link via reflection, making it the most expensive check.

CPU Scaling (lower is better)
// NotFoundError is a custom error type, extracted from a chain via errors.As.
type NotFoundError struct {
	ID int
}

func (e *NotFoundError) Error() string {
	return "user " + strconv.Itoa(e.ID) + " not found"
}

func BenchmarkErrorsAs_run(b *testing.B) {
	// A two-level wrapped chain with a custom error type at the bottom.
	err := fmt.Errorf("handling request: %w", fmt.Errorf("loading user: %w", &NotFoundError{ID: 42}))
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		var target *NotFoundError
		sinkBool = errors.As(err, &target)
	}
}
1 CPU
5.8xslower(480%)thanErrors Is
4.3xslower(328%)thanErrors New
1.5xfaster(46%)thanFmt Errorf
1.8xfaster(79%)thanFmt Errorf Wrap
46.9xslower(4587%)thanSentinel Check
32 CPUs
7.2xslower(622%)thanErrors Is
4.4xslower(341%)thanErrors New
1.7xfaster(66%)thanFmt Errorf
1.8xfaster(84%)thanFmt Errorf Wrap
44.2xslower(4319%)thanSentinel Check
#5

Fmt Errorf #

Builds an error with fmt.Errorf("loading user %d: %v", i, err). On top of the allocation, this pays for fmt's reflection-based formatting of the verbs. The %v verb flattens the cause into the message without keeping it in the chain.

CPU Scaling (lower is better)
func BenchmarkFmtErrorf_run(b *testing.B) {
	errNotFound := errors.New("user not found")
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		// %v formats the cause into the message but does not wrap it.
		sinkErr = fmt.Errorf("loading user %d: %v", i, errNotFound)
	}
}
1 CPU
1.5xslower(46%)thanErrors As
8.5xslower(747%)thanErrors Is
6.3xslower(525%)thanErrors New
1.2xfaster(22%)thanFmt Errorf Wrap
68.5xslower(6747%)thanSentinel Check
32 CPUs
1.7xslower(66%)thanErrors As
12xslower(1097%)thanErrors Is
7.3xslower(631%)thanErrors New
1.1xfaster(11%)thanFmt Errorf Wrap
73.2xslower(7225%)thanSentinel Check
#6

Fmt Errorf Wrap #

Slowest

Same as fmt.Errorf, but uses the %w verb, producing a wrapError that keeps a reference to the cause so errors.Is and errors.As can unwrap it later. This is the idiomatic way to add context while preserving the original error.

CPU Scaling (lower is better)
func BenchmarkFmtErrorfWrap_run(b *testing.B) {
	errNotFound := errors.New("user not found")
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		// %w keeps the cause in the chain for errors.Is / errors.As.
		sinkErr = fmt.Errorf("loading user %d: %w", i, errNotFound)
	}
}
1 CPU
1.8xslower(79%)thanErrors As
10.4xslower(937%)thanErrors Is
7.7xslower(665%)thanErrors New
1.2xslower(22%)thanFmt Errorf
83.8xslower(8278%)thanSentinel Check
32 CPUs
1.8xslower(84%)thanErrors As
13.3xslower(1230%)thanErrors Is
8.1xslower(712%)thanErrors New
1.1xslower(11%)thanFmt Errorf
81.4xslower(8038%)thanSentinel Check

Contributors