Skip to content

Stop building a backtrace in the default rescue handler - #2931

Closed
ericproulx wants to merge 1 commit into
masterfrom
perf/lazy-rescue-backtraces
Closed

Stop building a backtrace in the default rescue handler#2931
ericproulx wants to merge 1 commit into
masterfrom
perf/lazy-rescue-backtraces

Conversation

@ericproulx

Copy link
Copy Markdown
Contributor

Summary

Middleware::Error#default_rescue_handler renders any exception matched by rescue_from :all, or by rescue_from SomeError given without a block, a common way to turn unexpected errors into 500s. It passed backtrace: exception.backtrace to the error payload. Exception#backtrace builds the whole backtrace as Strings, which at a request's stack depth is the dearest part of rendering the error, and the result was then discarded unless the API had asked for rescue_from ..., backtrace: true.

#resolved_backtrace already falls back to original_exception.backtrace when a backtrace is wanted, and the handler passes original_exception, so the eager read goes. What an API that asked for the backtrace renders is unchanged.

Benchmarks

Median of 7 interleaved subprocess rounds against master, Ruby 4.0.6, no JIT:

response delta
500 from rescue_from :all (no block) +92.8%
500 from a rescue_from :all block calling error! (control, does not use the default handler) +0.6% (noise)

Not changed

Formatter builds its payload the same way in two places, for an InvalidFormatter and for a custom parser raising a non-Grape error, but a spec (formatter_spec.rb:509) pins the backtrace of the payload it throws, and neither path is hot: Grape's own parsers turn a parse failure into InvalidMessageBody, which does not go through that branch (a malformed JSON body measured +0.1% with both changed). They are left as they are.

Behaviour

Byte-identical to master over a 30-case matrix of rescue_from :all APIs, with and without backtrace: true and original_exception: true, in JSON, txt and XML, for raised Ruby and Grape exceptions and malformed bodies: 18 of the responses render a backtrace, identical once paths are normalized. The 228-case error matrix from #2929 is unchanged too.

Test plan

  • Full RSpec suite passes locally.
  • RuboCop clean.
  • Mutation-checked: also dropping original_exception: from the handler, so no backtrace is available when one is asked for, fails 5 specs.
  • CI green.

🤖 Generated with Claude Code

`Middleware::Error#default_rescue_handler`, which renders any exception
matched by `rescue_from :all` or `rescue_from SomeError` given without a
block, passed `backtrace: exception.backtrace` to the error payload.
`Exception#backtrace` builds the whole backtrace as Strings, which at a
request's stack depth is the dearest part of rendering the error, and
the result was then discarded unless the API had asked for
`rescue_from ..., backtrace: true`.

`#resolved_backtrace` already falls back to
`original_exception.backtrace` when a backtrace is wanted, and the
handler passes `original_exception`, so the eager read goes. A 500 from
`rescue_from :all` went from 31.5k to 60.8k requests per second.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ericproulx
ericproulx force-pushed the perf/lazy-rescue-backtraces branch from e6e5f50 to 5d6b4f7 Compare September 11, 2026 08:37
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

Danger Report

No issues found.

View run

@ericproulx

Copy link
Copy Markdown
Contributor Author

Closing in favour of #2936, which combines #2922#2934 into one PR, re-benchmarked as a whole against master. The write-up here (behaviour matrix, mutation results) still describes this part of the change.

@ericproulx ericproulx closed this Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant