fix(beam): keep the original stacktrace when an async error is re-raised#4818
Merged
Conversation
An error inside an async computation does not propagate as an exception: it is
caught where it happens and handed to the context's `on_error` as a bare term.
By the time `run_synchronously` re-raised it, the original stacktrace was gone,
so `erlang:error/1` built a fresh one and every async failure was reported at
the re-raise line rather than where it happened:
before: top frame: {fable_async,run_synchronously,2,[...,{line,147}]}
after: top frame: {st_demo,boom,0,[{file,"st_demo.erl"},{line,4}]}
Each catch site in `fable_async`/`fable_async_builder` that hands an error
onward now parks the stacktrace it caught, and the re-raise restores it when it
belongs to that error. Errors that crossed a process boundary or were rewritten
by `wrap_error` fall back to a fresh stacktrace.
A parked entry is tagged with the enclosing `run_synchronously` call's token so
only that call can consume it. Untagged, a stacktrace parked for an error that
was later handled (a `try_with` whose handler succeeded) would linger in the
process dictionary and could be restored onto an unrelated later error that
happened to be an equal term — attaching a plausible but wrong stacktrace, the
very failure this mechanism exists to remove. `try_with` also drops the parked
entry on the paths where it handles the error rather than propagating it.
Adds native Erlang tests: these assert on Erlang stack frames, which have no
counterpart on .NET, so they cannot be expressed in the F# suite that runs
there first.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Split out of #4817, where it rode along as an unrelated second fix.
Problem
An error inside an async computation does not propagate as an exception: it is caught where it happens and handed to the context's
on_erroras a bare term. By the timerun_synchronouslyre-raised it, the original stacktrace was long gone, soerlang:error/1built a fresh one and every async failure was reported at the re-raise line — a line whose only job is to re-raise.Fix
Each catch site in
fable_async/fable_async_builderthat hands an error onward parks the stacktrace it caught, and the re-raise restores it when it belongs to that error. Errors that crossed a process boundary, or that were rewritten bywrap_error, fall back to a fresh stacktrace.A parked entry is tagged with the enclosing
run_synchronouslycall's token, and only that call can consume it. Untagged, a stacktrace parked for an error that was later handled (atry_withwhose handler succeeded) would linger in the process dictionary and could be restored onto an unrelated later error that happened to be an equal term — attaching a plausible but wrong stacktrace, which is the very failure this mechanism exists to remove.try_withadditionally drops the parked entry on the paths where it handles an error rather than propagating it.start_with_continuationsdeliberately does not park: the caller suppliesOnErrorand receives the error term directly, so there is no re-raise to restore onto. That is now stated in a comment rather than left as a silent gap.Tests
tests/Beam/erl/async_stacktrace_tests.erl— native Erlang rather than F#, because the assertions are about Erlang stack frames, which have no counterpart on .NET (the F# suite runs there first). Picked up automatically byerl_test_runner.Each test discriminates against the relevant baseline:
mainVerification
In-tree BEAM suite on this branch: 2636 passed, 0 failed (2633 baseline + the 3 new tests), plus both entry-point program tests.
🤖 Generated with Claude Code