Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions crates/perry-runtime/src/promise/async_step.rs
Original file line number Diff line number Diff line change
Expand Up @@ -618,9 +618,51 @@ extern "C" fn async_step_fulfill_thunk(
});
let result = crate::closure::js_closure_call2(step, value, false_bits);
INLINE_TRAP.with(|c| c.set(prev));
forward_swallowed_rejection(result, captured_trap_next);
result
}

/// #5941: a thunk-resumed step that exits through its internal catch arm
/// (a `throw` after the await, or an awaited rejection with no matching
/// catch route) returns a FRESH rejected promise
/// (`return Promise.reject(e)` in the step body's catch) — historically
/// DISCARDED by the thunks, so the machine's real result promise (the
/// captured per-activation `trap_next`, #5485) stayed Pending and every
/// awaiter of the async fn hung instead of observing the throw. On the
/// first_call path the wrapper returns the rejected promise to the caller
/// directly, so only the resumed path leaked. Forward the rejection into
/// the activation's result. The normal resume paths return `trap_next`
/// itself (chain/done reuse) and are skipped by the identity check;
/// `js_async_step_done`'s non-reuse arm returning a user
/// `return <rejected promise>` also lands here, which matches spec (the
/// async fn's result rejects with the inner reason).
fn forward_swallowed_rejection(result: f64, trap_next: *mut Promise) {
if trap_next.is_null() {
return;
}
let bits = result.to_bits();
if bits & 0xFFFF_0000_0000_0000 != 0x7FFD_0000_0000_0000 {
return;
}
if js_value_is_promise(result) == 0 {
return;
}
let ret = (bits & 0x0000_FFFF_FFFF_FFFF) as *mut Promise;
if ret.is_null() || ret == trap_next {
return;
}
unsafe {
if (*ret).state != PromiseState::Rejected || (*trap_next).state != PromiseState::Pending {
return;
}
let reason = (*ret).reason;
// The rejection is consumed by forwarding it into the result —
// the discarded wrapper must not be reported as unhandled.
crate::promise::mark_rejection_handled(ret);
js_promise_reject(trap_next, reason);
}
}

extern "C" fn async_step_reject_thunk(
closure: *const crate::closure::ClosureHeader,
value: f64,
Expand All @@ -643,6 +685,7 @@ extern "C" fn async_step_reject_thunk(
});
let result = crate::closure::js_closure_call2(step, value, true_bits);
INLINE_TRAP.with(|c| c.set(prev));
forward_swallowed_rejection(result, captured_trap_next);
result
}

Expand Down
28 changes: 19 additions & 9 deletions crates/perry-runtime/src/promise/then.rs
Original file line number Diff line number Diff line change
Expand Up @@ -699,15 +699,25 @@ pub extern "C" fn js_promise_resolve_with_promise(outer: *mut Promise, inner: *m
ptr::null_mut(),
capture_context(),
);
// Keep the original next-nulling: the forwarder handles adoption,
// and leaving a stale `inner.next` chained to `outer` can form a
// resolution cycle ("Chaining cycle detected"). The awaiter's
// reaction lives in on_fulfilled (untouched above), not in `next`.
store_promise_next_slot(
inner,
std::ptr::addr_of_mut!((*inner).next),
ptr::null_mut(),
);
// #5941: null `inner.next` ONLY when it already points at
// `outer` — the re-adoption case where leaving it would have
// the runner's settle propagation resolve `outer` on top of
// the forwarder ("Chaining cycle detected"). Any OTHER
// promise in `inner.next` is an EARLIER dependent's chain —
// a prior adoption's fast-path edge (the `store_promise_next_slot`
// above) or a `.then` child — and the old unconditional null
// severed it: with two dependents adopting one shared inner
// promise (Next.js RSC render, shared React work promises),
// the second adoption orphaned the first dependent, its
// async-step machine's result never settled, and the render
// parked forever (the #5437/#5941 dynamic-route deadlock).
if outer == (*inner).next {
store_promise_next_slot(
inner,
std::ptr::addr_of_mut!((*inner).next),
ptr::null_mut(),
);
}
}
}
}
Expand Down
Loading
Loading