Skip to content

Preserve backtrace through JsError → JsValue → JsError round-trip#4609

Merged
jedel1043 merged 9 commits intoboa-dev:mainfrom
AlbertMarashi:include-backtrace-in-errors
Feb 24, 2026
Merged

Preserve backtrace through JsError → JsValue → JsError round-trip#4609
jedel1043 merged 9 commits intoboa-dev:mainfrom
AlbertMarashi:include-backtrace-in-errors

Conversation

@AlbertMarashi
Copy link
Contributor

Summary

Closes #4608
Supersedes #4607

  • Errors caught by internal exception handlers (e.g. async module evaluation) now preserve their backtrace through the JsError → JsValue → JsError round-trip via promise rejection
  • Stores the backtrace on the Error object in JsError::into_opaque (both Native and Opaque variants) and recovers it in JsError::from_opaque
  • Moves backtrace capture in handle_error before the exception handler check, so catchable errors also get a backtrace before being intercepted by internal handlers
  • Adds regression tests

Details

Previously, the backtrace was only captured for uncatchable errors (inside if !err.is_catchable()) and for catchable errors that reached handle_throw(). Errors intercepted by internal handlers (e.g. the module async wrapper) were stored as pending_exception before either path could capture a backtrace. When these errors were later converted to JsValue for promise rejection, the backtrace was lost entirely.

This PR:

  1. Captures the backtrace unconditionally in handle_error, before the exception handler check
  2. Stores the backtrace on the Error object during into_opaque (for both Repr::Native and Repr::Opaque errors) so it survives conversion to JsValue
  3. Recovers the backtrace in from_opaque by reading it back from the Error object

This approach was suggested by @jedel1043 in #4607 — extending the Error type with a backtrace field rather than injecting positions, which would be incorrect with nested try blocks.

Test plan

  • test_call_error_preserves_backtrace — calls undefined() at module top level, verifies backtrace with exact string match
  • test_nested_call_error_preserves_backtrace — nested foo → baz → import.meta.non_existent(), verifies full multi-frame backtrace
  • test_explicit_throw_preserves_backtrace — explicit throw new Error("test") inside a function, verifies backtrace frames are present

Made with Cursor

@AlbertMarashi
Copy link
Contributor Author

@jedel1043 requesting review

@github-actions
Copy link

github-actions bot commented Feb 7, 2026

Test262 conformance changes

Test result main count PR count difference
Total 52,862 52,862 0
Passed 49,497 49,497 0
Ignored 2,261 2,261 0
Failed 1,104 1,104 0
Panics 0 0 0
Conformance 93.63% 93.63% 0.00%

@randomboi404
Copy link
Contributor

I recommend you to fix the issues in your code and make it pass the CI otherwise it can't be merged.

@jedel1043 jedel1043 added bug Something isn't working vm Issues and PRs related to the Boa Virtual Machine. waiting-on-author Waiting on PR changes from the author labels Feb 7, 2026
@AlbertMarashi
Copy link
Contributor Author

@randomboi404 @jedel1043 done

@AlbertMarashi
Copy link
Contributor Author

AlbertMarashi commented Feb 24, 2026

interesting. it appears that backtraces (maybe a feature) seems to differ on the CI...

to avoid flakiness with source code changes in the (native) part of the backtraces, I might strip them out or use another approach - (so ideally when the source code is changed it doesn't painstakingly cause this test to fail also)


Reproduced it. So:

  • cargo test -p boa_engine — only boa_engine's own features, native-backtrace is off → tests pass
  • cargo test --workspace (what CI does) — Cargo unifies features across the workspace, the CLI crate enables native-backtrace on boa_engine(native at file:line:col) appears instead of (native) → exact string match fails

AlbertMarashi and others added 5 commits February 25, 2026 02:57
Errors caught by internal exception handlers (e.g. async module
evaluation) lost their backtrace when going through promise rejection,
because the backtrace lived only on the JsError and was discarded
during conversion to JsValue.

- Store the backtrace on the Error object in `JsError::into_opaque`
  (both Native and Opaque variants) so it survives the round-trip
- Recover the backtrace in `JsError::from_opaque`
- Move backtrace capture in `handle_error` before the exception handler
  check so catchable errors also get a backtrace
- Add regression tests for implicit TypeError, nested calls, and
  explicit throw

Closes boa-dev#4608
Supersedes boa-dev#4607

Co-authored-by: Cursor <cursoragent@cursor.com>
Collapse nested `if let` blocks into chained `let` conditions
to satisfy the `clippy::collapsible_if` lint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add backticks around type names in doc comment (clippy::doc_markdown)
- Remove unnecessary hashes from raw string literals (clippy::needless_raw_string_hashes)
The `native-backtrace` feature (enabled via workspace feature unification
from boa_cli) changes error output to include Rust source locations.
Strip native location info before comparing so tests pass in both modes.

Also add a sanity check test for context.eval() backtrace (relates to boa-dev#4475).
@AlbertMarashi AlbertMarashi force-pushed the include-backtrace-in-errors branch from 5455b07 to 5373239 Compare February 24, 2026 16:29
@codecov
Copy link

codecov bot commented Feb 24, 2026

Codecov Report

❌ Patch coverage is 92.45283% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.91%. Comparing base (6ddc2b4) to head (18373eb).
⚠️ Report is 672 commits behind head on main.

Files with missing lines Patch % Lines
core/engine/src/error/tests.rs 89.28% 3 Missing ⚠️
core/engine/src/builtins/error/mod.rs 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4609      +/-   ##
==========================================
+ Coverage   47.24%   56.91%   +9.66%     
==========================================
  Files         476      549      +73     
  Lines       46892    60129   +13237     
==========================================
+ Hits        22154    34222   +12068     
- Misses      24738    25907    +1169     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…ding

Consolidate backtrace tests from the separate integration test file
(core/engine/tests/errors.rs) into the in-crate unit test module
(builtins::error::tests) for better access to internal types. Remove
duplicate `let path` binding that caused an unused variable warning.
@AlbertMarashi
Copy link
Contributor Author

@jedel1043 should be ready to merge now.

Backtrace tests exercise JsError behavior, not the Error builtin,
so they belong in the error module. Convert error.rs to a module
directory and add the backtrace tests there. Keep builtin-specific
tests (error_to_string, error_names, error_lengths) in builtins/error.
Copy link
Member

@jedel1043 jedel1043 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice, thank you for the fix!

@jedel1043 jedel1043 added this pull request to the merge queue Feb 24, 2026
Merged via the queue into boa-dev:main with commit 27d95d7 Feb 24, 2026
17 checks passed
@AlbertMarashi AlbertMarashi deleted the include-backtrace-in-errors branch February 24, 2026 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working vm Issues and PRs related to the Boa Virtual Machine. waiting-on-author Waiting on PR changes from the author

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Errors caught by internal handlers (e.g. module async wrapper) lack source position/span info

3 participants