ITADN

AsyncLocalStorage context is lost inside a thenable's then() when the thenable is returned from an async function after an await

#34266ClosedIlyaSemenov 创建于 2026-07-15
bugneeds triage
I
IlyaSemenovcommented
### What version of Bun is running? 1.3.14+0d9b296af (also reproduces on 1.3.10 and on canary 1.4.0-canary.1+be77b6528) ### What platform is your computer? Darwin 25.5.0 arm64 arm ### What steps can reproduce the bug? Run the following with `bun run repro.mjs`: ```js import { AsyncLocalStorage } from "node:async_hooks"; const als = new AsyncLocalStorage(); async function f() { await null; // any await before the return triggers the bug return { then(resolve) { console.log("store in then(): ", als.getStore()); resolve(42); }, }; } await als.run("CTX", async () => { const value = await f(); console.log("store after await: ", als.getStore(), "| value:", value); }); ``` The key ingredient is `return <thenable>` from an async function *after at least one await*. The thenable's `then()` is then invoked (via PromiseResolveThenableJob) with an empty AsyncLocalStorage context. If `then` is defined as a getter, the `Get(thenable, "then")` also happens with an empty context. ### What is the expected behavior? Node.js (24.16.0) prints: ``` store in then(): CTX store after await: CTX | value: 42 ``` The thenable's `then()` should run with the AsyncLocalStorage context that was active in the async function that returned it. ### What do you see instead? Bun prints: ``` store in then(): undefined store after await: CTX | value: 42 ``` The awaiting continuation keeps the context, but the code inside the thenable's `then()` runs with no context at all (getStore() returns undefined for every AsyncLocalStorage instance, not just this one). ### Additional information All neighboring cases preserve the context correctly, which makes this easy to miss: `await thenable` directly — OK; `return thenable` from an async function with NO await before it — OK; `Promise.resolve(thenable)` — OK; `new Promise(r => r(thenable))` — OK. Only "async function suspended at least once, then returns a thenable" loses the context inside `then()`. This looks like a remaining edge of #6393. That issue covered the direct `await thenable` case and was closed as no longer reproducible — which matches what I see: the direct-await path is fixed on 1.3.14, but the "return thenable after a suspension" path still runs `then()` (and, for a getter-defined `then`, the `Get(thenable, "then")` itself) with an empty context. Real-world impact: ORMs and query builders (Knex, Objection.js, Orchid ORM / pqb) expose queries as thenables and start executing the query inside `then()`, reading the current transaction from AsyncLocalStorage there. A very common pattern like ```js async function saveThing(data) { await somethingElse(); return db.thing.insert(data); // query builder = thenable } ``` silently executes the query on a NEW pooled connection outside the ambient transaction (no error — just wrong connection), which in my case led to FK violation because an uncommitted parent row was not visible. Node.js runs the same code correctly.
关闭于 2026-07-15 1 条评论