Error Interface Cost and Wrapping

Updated

September 8, 2026

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 types

Returning 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=. -benchmem
package 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.