Skip to content

feat(host): report unhandled promise rejections like pinned qjs - #24

Open
lfkdsk wants to merge 3 commits into
pocket-stack:mainfrom
lfkdsk:pr/host-unhandled-rejection
Open

lfkdsk wants to merge 3 commits into
pocket-stack:mainfrom
lfkdsk:pr/host-unhandled-rejection

Conversation

@lfkdsk

@lfkdsk lfkdsk commented Sep 17, 2026

Copy link
Copy Markdown

What

The qjs CLI now installs the host promise-rejection tracker the way quickjs-libc.c does (js_std_promise_rejection_tracker / js_std_promise_rejection_check): a rejection that still has no handler when the job queue drains prints Possibly unhandled promise rejection: <reason> to stderr through the JS_PrintValue path (Error reasons keep their stack) and the process exits 1. A handler attached before or during the drain removes the entry. --no-unhandled-rejection is added with the same default as pinned qjs, and draining continues after a job throws, matching js_std_loop.

Previously these rejections were silent and the process exited 0.

Verification

  • 12 byte-exact golden/differential cases against the pinned QuickJS 2026-06-04 oracle: Error and primitive reasons, stack frames, FIFO report order, pre- and mid-drain handling, derived/nested/rethrown rejections, --no-unhandled-rejection, and the modules case where a handled import() still reports the inner module evaluation rejection.
  • cargo test --locked --workspace --all-targets: 2846 pass / 0 fail; check-rust-only.sh and cargo fmt --check green.
  • Independent cross-model review (fleet task 952) reproduced the differential matrix.

Part of the parity work tracked from the docs/parity.md host-library row; no Test262 receipts touched.

lfkdsk and others added 2 commits September 17, 2026 13:29
Install the host promise rejection tracker in the qjs CLI so rejections
that still have no handler when the job queue drains print
"Possibly unhandled promise rejection: <reason>" to stderr (via the
JS_PrintValue path, so Error reasons keep their attached stack) and the
process exits 1, matching js_std_promise_rejection_tracker and
js_std_promise_rejection_check in quickjs-libc.c. A handler attached
before or during the drain removes the entry. Add --no-unhandled-rejection
(default still reports) and keep draining after a job JavaScript exception
as js_std_loop does.

Ported to the workspace layout: src/main.rs -> apps/cli/src/main.rs,
imports via quickjs_oxide::engine::api, and the PendingJobOutcome/
into_error job API.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Add 12 byte-exact golden/differential cases covering Error and primitive
reasons, stack frames, FIFO report order, pre- and mid-drain handling,
derived/nested/rethrown rejections, plus --no-unhandled-rejection and the
modules-20 case where an handled import() still reports the inner module
evaluation rejection. Differential assertions run against QJS_ORACLE.

Ported to the workspace layout: the CLI integration target now lives at
apps/cli/tests/cli.rs with submodules under tests/cli/, so the cases are
in tests/cli/rejections.rs and reuse the shared run_cli/ModuleFixture
helpers via `super::*`.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@lfkdsk
lfkdsk force-pushed the pr/host-unhandled-rejection branch from def0e14 to c296ec4 Compare September 17, 2026 21:27
lfkdsk added a commit to lfkdsk/quickjs-oxide that referenced this pull request Sep 18, 2026
Old-to-new path mapping (src/main.rs -> apps/cli/src/main.rs;
tests/cli.rs -> apps/cli/tests/cli.rs + tests/cli/rejections.rs),
mechanical adaptations (engine::api imports, PendingJobOutcome/
into_error job API, profiling session), all gate outputs, and the
12 byte-exact pinned-oracle differential results.

Co-Authored-By: Claude Code <noreply@anthropic.com>
lfkdsk added a commit to lfkdsk/quickjs-oxide that referenced this pull request Sep 18, 2026
… and thenable

Close the two byte-level mismatches the cross-family review of pocket-stack#24 found in
the `Possibly unhandled promise rejection:` line, making the differential
surface 66/66 byte-exact with pinned QuickJS 2026-06-04.

1. Failed dynamic import no longer doubles the message. Upstream's host
   js_module_loader throws the final ReferenceError itself
   ("could not load module filename '%s'", quickjs-libc.c:699), so the engine
   wrapper is never reached. The CLI file loader now raises a true
   realm-intrinsic JS ReferenceError via ModuleLoaderError::exception instead
   of a Message error the engine re-wrapped. Add public
   Context::new_native_error so an embedder can build intrinsic (global
   tamper-immune) native errors.

2. A throwing `then` getter no longer prints an extra `at <anonymous>
   (native)` frame. QuickJS invokes the PROMISE_RESOLVE/REJECT_FUNCTION class
   call handlers without pushing a JSStackFrame; mark the PromiseResolving
   native frame backtrace_hidden (same mechanism as the Iterator.next raw
   fast path) so the frame is kept for realm/budget accounting but omitted
   from backtraces.

Add both probes to apps/cli/tests/cli/rejections.rs (golden + oracle
differential), plus a module-file case covering the unhandled and caught
channels. Verified by mutation tests and a 396-variant test262
Promise/resolve + dynamic-import/catch subset whose outcome set is identical
before/after (74 pass, no regressions).

Co-Authored-By: Claude Code <noreply@anthropic.com>
… and thenable

Close the two byte-level mismatches the cross-family review of pocket-stack#24 found in
the `Possibly unhandled promise rejection:` line, making the differential
surface 66/66 byte-exact with pinned QuickJS 2026-06-04.

1. Failed dynamic import no longer doubles the message. Upstream's host
   js_module_loader throws the final ReferenceError itself
   ("could not load module filename '%s'", quickjs-libc.c:699), so the engine
   wrapper is never reached. The CLI file loader now raises a true
   realm-intrinsic JS ReferenceError via ModuleLoaderError::exception instead
   of a Message error the engine re-wrapped. Add public
   Context::new_native_error so an embedder can build intrinsic (global
   tamper-immune) native errors.

2. A throwing `then` getter no longer prints an extra `at <anonymous>
   (native)` frame. QuickJS invokes the PROMISE_RESOLVE/REJECT_FUNCTION class
   call handlers without pushing a JSStackFrame; mark the PromiseResolving
   native frame backtrace_hidden (same mechanism as the Iterator.next raw
   fast path) so the frame is kept for realm/budget accounting but omitted
   from backtraces.

Add both probes to apps/cli/tests/cli/rejections.rs (golden + oracle
differential), plus a module-file case covering the unhandled and caught
channels. Verified by mutation tests and a 396-variant test262
Promise/resolve + dynamic-import/catch subset whose outcome set is identical
before/after (74 pass, no regressions).

Co-Authored-By: Claude Code <noreply@anthropic.com>
@lfkdsk
lfkdsk force-pushed the pr/host-unhandled-rejection branch from 6a1e4e6 to 275bb67 Compare September 18, 2026 19:03
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