Go: Sentinel errors and errors.Is() slow your code down by 3000%

Benchmarks of Go’s `errors.Is` and sentinel error patterns suggest they can be several times slower than simple boolean checks, especially in tight, CPU‑bound loops, prompting scrutiny of Go’s idiomatic error handling. Commenters note that much of the real cost comes from constructing and wrapping errors (often with allocations and reflection), and that in I/O‑bound or typical service code these nanosecond‑level differences are usually irrelevant compared to gains in clarity and debuggability. The thread also touches on how future Go releases and alternative patterns (like bool “ok” returns) might improve performance without sacrificing error semantics.

Performance of errors.Is in Go

  • Benchmarks showed errors.Is significantly slower than a boolean check; later correction reduced the claim from “3000% / 30x” to roughly “500% / 5x” for the microbenchmark.
  • Several commenters stress the absolute times are tiny (single‑digit tens of nanoseconds) and usually dwarfed by I/O, but acknowledge it can matter in very hot in‑memory loops.
  • Key costs:
    • Pre‑fix: no fast path for err == nil, so even the happy path paid reflection and tree‑walk overhead.
    • Implementation uses reflection and walks wrapped error chains.
    • Inlining behavior matters; when functions were inlined, some benchmark variants were unrealistically optimized away.

Go design & upcoming improvements

  • Newer (unreleased) Go adds a fast check for err == nil || target == nil in errors.Is, expected to reduce overhead on the happy path.
  • Some discuss possible further optimizations (splitting reflection out to allow better inlining), and an ongoing inliner improvement effort.

Sentinel errors vs booleans (ok pattern)

  • Debate over whether “not found” should be a sentinel error or a bool like Go map lookups.
  • Arguments for bool:
    • Faster in tight loops.
    • “Not found” is often a normal outcome, not truly exceptional.
  • Arguments for errors:
    • Nice symmetry with other failure modes.
    • Allows including which value/key was missing.
    • Sentinel errors already used in stdlib (e.g., sql.ErrNoRows, io.EOF), though usually in slow operations.

Error construction vs inspection

  • Multiple comments note most time is spent constructing errors (formatting strings, allocations, multiple wraps), not in errors.Is itself.
  • Deeply wrapped errors (many %w layers) are especially expensive and often produce unreadable messages.
  • Some advocate wrapping on every layer for debuggability and structured logs, then only optimizing hot spots once profiling shows a problem.
  • Others argue for treating errors as mostly opaque, avoiding pervasive wrapping and using stack traces and structured logging instead.

Broader language comparisons & philosophy

  • Rust: discussion about exposing dependency error types, #[non_exhaustive], and desire for exhaustive matching vs semver stability.
  • Java/C#/C++/JS: long side thread on “error vs exception” semantics and checked vs unchecked behavior.
  • Some see Go’s error handling and if err != nil patterns (plus variable shadowing with ok/err) as awkward; others consider it a reasonable trade‑off for simplicity and clarity.
  • Several emphasize profiling and avoiding premature optimization; errors.Is is rarely the dominant cost in real systems.