AsyncLocalStorage context is lost inside a thenable's then() when the thenable is returned from an async function after an await
bugneeds triage
### 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 条评论