Skip to content

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

Open
AlbertMarashi wants to merge 2 commits intoboa-dev:mainfrom
AlbertMarashi:include-backtrace-in-errors
Open

Preserve backtrace through JsError → JsValue → JsError round-trip#4609
AlbertMarashi wants to merge 2 commits intoboa-dev:mainfrom
AlbertMarashi:include-backtrace-in-errors

Conversation

@AlbertMarashi
Copy link

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 and others added 2 commits February 7, 2026 15:06
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>
@AlbertMarashi
Copy link
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,471 49,471 0
Ignored 2,249 2,249 0
Failed 1,142 1,142 0
Panics 0 0 0
Conformance 93.59% 93.59% 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
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