Skip to content

fix(runtime): adopt returned promise state on async tail-return (#4828)#4832

Merged
proggeramlug merged 1 commit into
mainfrom
fix/4828-async-tail-promise
Jun 9, 2026
Merged

fix(runtime): adopt returned promise state on async tail-return (#4828)#4832
proggeramlug merged 1 commit into
mainfrom
fix/4828-async-tail-promise

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Fixes #4828.

Problem

An async function whose tail expression returns an un-awaited promise (return someAsyncCall()) corrupted the resolved value. Awaiting the outer fn produced the inner Promise object itself — typeof === "object", JSON.stringify === "", every property undefined — instead of the inner promise's eventual value. Adding an explicit await (const r = await someAsyncCall(); return r;) worked. This is a very common JS pattern, so impact is broad (a createCustomer flow returned "" to its caller, breaking redirects).

Root cause

js_async_step_done (crates/perry-runtime/src/promise/async_step.rs) settles the async fn's result promise two ways. The slow path uses js_promise_resolved(value), which performs ECMAScript thenable/promise adoption (native-Promise short-circuit #2823, thenable assimilation #586). The in-place reuse fast path — taken in steady state when the microtask runner has stashed the result promise in INLINE_TRAP.trap_next — bypassed adoption with the low-level js_promise_resolve(trap_next, value), storing the returned Promise object as the literal resolution value. The sibling js_async_step_chain (the await x path) already adopts, which is why explicit await masked the bug.

Fix

Added resolve_trap_next_with_adoption, mirroring js_promise_resolved's adoption probes (primitive short-circuit, native-Promise chain via js_promise_resolve_with_promise, thenable assimilation) but settling the existing trap_next rather than allocating a fresh promise — so the runner's self-chain fast path still fires. The fast path now calls it instead of the raw js_promise_resolve.

Verification

Compiled and ran the issue's repro (test-files/test_issue_4828_async_tail_promise.ts). Before: broken: ... json="" .id=undefined. After, both lines identical and correct:

broken: typeof=object json={"id":"abc","n":1} .id="abc"
fixed:  typeof=object json={"id":"abc","n":1} .id="abc"

An async fn whose tail expression returns an un-awaited promise
(`return someAsyncCall()`) corrupted the resolved value: awaiting the
outer fn yielded the inner Promise object itself (typeof "object",
JSON.stringify "", every property undefined) instead of the inner
promise's eventual value.

Root cause: js_async_step_done's in-place reuse fast path settled the
result promise (INLINE_TRAP.trap_next) with a raw js_promise_resolve,
skipping the ECMAScript thenable/promise adoption that the slow path
(js_promise_resolved) performs. The sibling js_async_step_chain (the
`await x` path) already adopts, which is why explicit `await` masked it.

Fix: resolve_trap_next_with_adoption mirrors js_promise_resolved's
adoption probes (primitive short-circuit, native-Promise chain via
js_promise_resolve_with_promise, thenable assimilation) but settles the
existing trap_next so the runner's self-chain fast path still fires.
@proggeramlug proggeramlug force-pushed the fix/4828-async-tail-promise branch from 31cc634 to 9006a3b Compare June 9, 2026 10:07
@proggeramlug proggeramlug merged commit 3f24466 into main Jun 9, 2026
13 checks passed
@proggeramlug proggeramlug deleted the fix/4828-async-tail-promise branch June 9, 2026 10:29
proggeramlug pushed a commit that referenced this pull request Jun 9, 2026
…omise adoption

Release-sync bump folding in #4834 (fully-static musl Linux target) and #4832 (async tail-return promise state adoption) that landed after the v0.5.1149 changelog entry.
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.

async: returning an un-awaited promise (return asyncFn()) corrupts the resolved value to ""

1 participant