Skip to content

AsyncLocalStorage: the store from an awaited als.run() stays visible after it returns #53

Closed

Description

@hassidan

Test I ran

node als_leak.ts
slice run als_leak.ts
als_leak.ts
import { AsyncLocalStorage } from "node:async_hooks";
const show = async (k: string, f: () => unknown) => { let v; try { v = await Promise.race([Promise.resolve().then(f), new Promise((r) => setTimeout(() => r("NEVER SETTLED (2000 ms)"), 2000))]); } catch (e: any) { v = "THROWS " + e.name + ": " + String(e.message).slice(0, 100); } console.log(k.padEnd(52) + JSON.stringify(v)); };
(async () => {
  const als = new AsyncLocalStorage<string>();
  await als.run("request-A", async () => { await new Promise((r) => setTimeout(r, 2)); });
  await show("getStore() after `await als.run('request-A', asyncFn)`", () => String(als.getStore()));
  await show("getStore() inside a later, unrelated setTimeout", () => new Promise((res) => setTimeout(() => res(String(als.getStore())), 5)));
  await show("getStore() inside a later, unrelated promise job", () => Promise.resolve().then(() => String(als.getStore())));
  const als2 = new AsyncLocalStorage<string>(); als2.run("sync", () => {});
  await show("getStore() after a synchronous als.run(...)", () => String(als2.getStore()));
})();

Result

Node v24.22.2:

getStore() after `await als.run('request-A', asyncFn)`"undefined"
getStore() inside a later, unrelated setTimeout     "undefined"
getStore() inside a later, unrelated promise job    "undefined"
getStore() after a synchronous als.run(...)         "undefined"

Slice v0.10.7:

getStore() after `await als.run('request-A', asyncFn)`"request-A"
getStore() inside a later, unrelated setTimeout     "request-A"
getStore() inside a later, unrelated promise job    "request-A"
getStore() after a synchronous als.run(...)         "undefined"

What is wrong

After await als.run("request-A", asyncFn) completes, code outside the run() scope still sees "request-A": in the awaiting function's continuation, in a later unrelated setTimeout, and in a later promise job. A synchronous als.run() restores correctly.

Propagation inside run() is correct (checked across await, setTimeout, queueMicrotask, and two concurrent run() calls keep separate stores). The defect is that the store is not restored for the code that awaited the run(). Since the store typically holds request-scoped data (user, tenant, trace id), whatever runs next in that continuation inherits the previous request's context.

Environment

  • Slice v0.10.7, commit ea7c70c, built with cargo +1.96.0 build --release --locked --bin slice
  • Reference: Node.js v24.22.2 (same script, same machine)
  • Ubuntu 26.04, Linux 7.0, x86_64
  • The same output is produced by the v0.10.5 tag, so this is not a regression in 0.10.6/0.10.7.

Activity

  1. siktec-lab commented on Nov 11, 2026

    @siktec-lab
    Collaborator

    Addressed in #67 (�0.10.11): implemented HostMakeJobCallback and HostCallJobCallback on SliceHostHooks in �oa_realm.rs so AsyncLocalStorage snapshots are captured at .then() / �wait registration time (Promise::perform_promise_then) and restored across promise reaction jobs, external deferred promises, and timer callbacks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions