Skip to the content

TC39 AsyncContext · Node.js AsyncLocalStorage

AsyncLocalStorage for the browser

Set a value one time, for example a request ID. All the code of that operation gets the value: after await, in promise callbacks, timers, event listeners and generators.

A Vite plugin or a Babel preset changes your async functions. A small runtime keeps the context. On Node.js, the package uses the native AsyncLocalStorage.

npm install async-browser-context

Read the guideOpen the debuggerGitHub

Works withViteBabelReactVitestOpenTelemetryNode.js

two-requests.jsLive in this page
Coderoot frame
import { AsyncLocalStorage } from "async-browser-context"; const requestId = new AsyncLocalStorage(); async function handle(id, user) {    await requestId.run(id, async () => {        log(`[${requestId.getStore()}] load ${user}`);        const name = await fetchUser(user);        log(`[${requestId.getStore()}] hello, ${name}`);        await save(name);    });} async function save(name) {    await sleep(5);    log(`[${requestId.getStore()}] saved ${name}`);} await Promise.all([    handle("r-1", "ada"),    handle("r-2", "grace"),]); 
ReadsNo read at this step
Frames
root frameno valuescurrentF1requestId = "r-1"F2requestId = "r-2"
Console
  1. No output yet.
root frame
requestId = "r-1"
requestId = "r-2"
Step 1 of 25

The page starts this code with the real library. Each step is a statement or the end of an await. Open the debugger, or write your own code in the playground.

A global variable is not sufficient

Two requests run at the same time. Each request keeps its ID in a variable, and writes the ID to the log after each await.

With one global variable, the second request changes the value while the first request waits. After the await, the first request writes the ID of the second request. The library keeps one value for each request.

The page starts the two runs when it loads, with the same code. The lines below are the real output.

A global variable2 of 6 lines have the ID of the other request
  1. ✓[r-1] load ada
  2. ✓[r-2] load grace
  3. ✓[r-2] hello, Grace
  4. ✓[r-2] saved Grace
  5. ✗[r-2] hello, Ada
  6. ✗[r-2] saved Ada
async-browser-contextEach line has the ID of its request
  1. ✓[r-1] load ada
  2. ✓[r-2] load grace
  3. ✓[r-2] hello, Grace
  4. ✓[r-2] saved Grace
  5. ✓[r-1] hello, Ada
  6. ✓[r-1] saved Ada

How the library operates

  1. 1

    The transform

    A native await does not tell JavaScript code when the function continues. Thus, the Vite plugin or the Babel preset changes each async function into a generator. Each await becomes a yield, and the runtime operates the generator.

    Look at more examples of the transform

    async function handle(id) {
        const user = await load(id);
        log(requestId.getStore(), user);
    }
    import { coroutine as _coroutine } from "async-browser-context/runtime";
    function handle(_x) {
      return _handle.apply(this, arguments);
    }
    function _handle() {
      _handle = _coroutine(function* (id) {
        const user = yield load(id);
        log(requestId.getStore(), user);
      });
      return _handle.apply(this, arguments);
    }
  2. 2

    The frames

    Each run() makes a frame below the current frame. Before each step of a function, the runtime makes the frame of the function current. After the step, it sets the previous frame again. get() searches from the current frame up to the root.

    Read about contexts and frames

    root frameno valuesF1requestId = "r-1"F2userId = "u-7"currentF3requestId = "r-1a"foundfound
    get() of requestId searched F2 → F1 and found "r-1" in F1.
  3. 3

    The patches

    A callback gets the context of the code that registered it. The runtime patches the APIs that call callbacks later. The patches keep the names, the lengths and the source text of the native functions.

    Read the list of patched APIs

    • PromisesthencatchfinallyPromise.all
    • TimerssetTimeoutsetIntervalqueueMicrotaskrequestAnimationFramerequestIdleCallbackscheduler.postTask
    • EventsaddEventListeneron… propertiesMediaQueryList
    • ObserversMutationObserverResizeObserverIntersectionObserverPerformanceObserverFinalizationRegistry
    • StreamsReadableStreamWritableStreamTransformStream
    • Other callbacksnavigator.lockstoBlobgeolocationstartViewTransitionArray.fromAsync
  4. 4

    The boundaries

    Code without the transform cannot get the context of a different operation. At most, it gets no context after its own native await. Where your code gives a callback to that code, one function keeps the context.

    Read about the boundaries

    • Gives a promise that your code awaitsNothing to do
    • Starts your callback from a patched APINothing to do
    • Starts your callback after its own awaitBind the callbackAsyncLocalStorage.bind(callback)
    • Keeps your callback in a list for laterBind the callbackAsyncLocalStorage.bind(callback)
    • Operates in a worker or an iframeSend the values, thenrun(value, fn)

What this site holds

async-browser-context: AsyncLocalStorage and the TC39 AsyncContext API for browsers. The source code is on GitHub.

To change a page, edit its file in site/content/. The writing style guide tells you how.

An AI model (Claude, from Anthropic) wrote most of the text and the code of this site and of the library, under the direction of the author. The tests and the STE linter examine them.