From 7ed2fdf3faefee63b0ccd5e65a0734e6096093cf Mon Sep 17 00:00:00 2001 From: jakecastelli <38635403+jakecastelli@users.noreply.github.com> Date: Sun, 29 Mar 2026 03:18:29 +1030 Subject: [PATCH 1/4] esm: fix typo in worker loader hook comment PR-URL: https://github.com/nodejs/node/pull/62475 Reviewed-By: Jacob Smith Reviewed-By: Luigi Pinca --- lib/internal/modules/esm/worker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/modules/esm/worker.js b/lib/internal/modules/esm/worker.js index fa5fa87d20efb1..e2553b253db830 100644 --- a/lib/internal/modules/esm/worker.js +++ b/lib/internal/modules/esm/worker.js @@ -37,7 +37,7 @@ const { isCascadedLoaderInitialized, getOrInitializeCascadedLoader } = require(' const { AsyncLoaderHooksOnLoaderHookWorker } = require('internal/modules/esm/hooks'); /** - * Register asynchronus module loader customization hooks. This should only be run in the loader + * Register asynchronous module loader customization hooks. This should only be run in the loader * hooks worker. In a non-loader-hooks thread, if any asynchronous loader hook is registered, the * ModuleLoader#asyncLoaderHooks are initialized to be AsyncLoaderHooksProxiedToLoaderHookWorker * which posts the messages to the async loader hook worker thread. From 5b6091ce31068e1c86f863273cdd397ecb47e3b7 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Sun, 29 Mar 2026 04:53:57 +0800 Subject: [PATCH 2/4] async_hooks: add using scopes to AsyncLocalStorage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds support for using scope = storage.withScope(data) to do the equivalent of a storage.run(data, fn) with using syntax. This enables avoiding unnecessary closures. PR-URL: https://github.com/nodejs/node/pull/61674 Reviewed-By: Anna Henningsen Reviewed-By: Gerhard Stöbich Reviewed-By: Bryan English Reviewed-By: James M Snell Reviewed-By: Chengzhong Wu --- doc/api/async_context.md | 164 ++++++++++++++ .../async_context_frame.js | 6 + .../async_local_storage/async_hooks.js | 6 + lib/internal/async_local_storage/run_scope.js | 31 +++ .../test-async-local-storage-run-scope.js | 202 ++++++++++++++++++ 5 files changed, 409 insertions(+) create mode 100644 lib/internal/async_local_storage/run_scope.js create mode 100644 test/parallel/test-async-local-storage-run-scope.js diff --git a/doc/api/async_context.md b/doc/api/async_context.md index dd3b6a834ec950..df6a6466fda98e 100644 --- a/doc/api/async_context.md +++ b/doc/api/async_context.md @@ -386,6 +386,110 @@ try { } ``` +### `asyncLocalStorage.withScope(store)` + + + +> Stability: 1 - Experimental + +* `store` {any} +* Returns: {RunScope} + +Creates a disposable scope that enters the given store and automatically +restores the previous store value when the scope is disposed. This method is +designed to work with JavaScript's explicit resource management (`using` syntax). + +Example: + +```mjs +import { AsyncLocalStorage } from 'node:async_hooks'; + +const asyncLocalStorage = new AsyncLocalStorage(); + +{ + using _ = asyncLocalStorage.withScope('my-store'); + console.log(asyncLocalStorage.getStore()); // Prints: my-store +} + +console.log(asyncLocalStorage.getStore()); // Prints: undefined +``` + +```cjs +const { AsyncLocalStorage } = require('node:async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); + +{ + using _ = asyncLocalStorage.withScope('my-store'); + console.log(asyncLocalStorage.getStore()); // Prints: my-store +} + +console.log(asyncLocalStorage.getStore()); // Prints: undefined +``` + +The `withScope()` method is particularly useful for managing context in +synchronous code where you want to ensure the previous store value is restored +when exiting a block, even if an error is thrown. + +```mjs +import { AsyncLocalStorage } from 'node:async_hooks'; + +const asyncLocalStorage = new AsyncLocalStorage(); + +try { + using _ = asyncLocalStorage.withScope('my-store'); + console.log(asyncLocalStorage.getStore()); // Prints: my-store + throw new Error('test'); +} catch (e) { + // Store is automatically restored even after error + console.log(asyncLocalStorage.getStore()); // Prints: undefined +} +``` + +```cjs +const { AsyncLocalStorage } = require('node:async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); + +try { + using _ = asyncLocalStorage.withScope('my-store'); + console.log(asyncLocalStorage.getStore()); // Prints: my-store + throw new Error('test'); +} catch (e) { + // Store is automatically restored even after error + console.log(asyncLocalStorage.getStore()); // Prints: undefined +} +``` + +**Important:** When using `withScope()` in async functions before the first +`await`, be aware that the scope change will affect the caller's context. The +synchronous portion of an async function (before the first `await`) runs +immediately when called, and when it reaches the first `await`, it returns the +promise to the caller. At that point, the scope change becomes visible in the +caller's context and will persist in subsequent synchronous code until something +else changes the scope value. For async operations, prefer using `run()` which +properly isolates context across async boundaries. + +```mjs +import { AsyncLocalStorage } from 'node:async_hooks'; + +const asyncLocalStorage = new AsyncLocalStorage(); + +async function example() { + using _ = asyncLocalStorage.withScope('my-store'); + console.log(asyncLocalStorage.getStore()); // Prints: my-store + await someAsyncOperation(); // Function pauses here and returns promise + console.log(asyncLocalStorage.getStore()); // Prints: my-store +} + +// Calling without await +example(); // Synchronous portion runs, then pauses at first await +// After the promise is returned, the scope 'my-store' is now active in caller! +console.log(asyncLocalStorage.getStore()); // Prints: my-store (unexpected!) +``` + ### Usage with `async/await` If, within an async function, only one `await` call is to run within a context, @@ -420,6 +524,64 @@ of `asyncLocalStorage.getStore()` after the calls you suspect are responsible for the loss. When the code logs `undefined`, the last callback called is probably responsible for the context loss. +## Class: `RunScope` + + + +> Stability: 1 - Experimental + +A disposable scope returned by [`asyncLocalStorage.withScope()`][] that +automatically restores the previous store value when disposed. This class +implements the [Explicit Resource Management][] protocol and is designed to work +with JavaScript's `using` syntax. + +The scope automatically restores the previous store value when the `using` block +exits, whether through normal completion or by throwing an error. + +### `scope.dispose()` + + + +Explicitly ends the scope and restores the previous store value. This method +is idempotent: calling it multiple times has the same effect as calling it once. + +The `[Symbol.dispose]()` method defers to `dispose()`. + +If `withScope()` is called without the `using` keyword, `dispose()` must be +called manually to restore the previous store value. Forgetting to call +`dispose()` will cause the store value to persist for the remainder of the +current execution context: + +```mjs +import { AsyncLocalStorage } from 'node:async_hooks'; + +const storage = new AsyncLocalStorage(); + +// Without using, the scope must be disposed manually +const scope = storage.withScope('my-store'); +// storage.getStore() === 'my-store' here + +scope.dispose(); // Restore previous value +// storage.getStore() === undefined here +``` + +```cjs +const { AsyncLocalStorage } = require('node:async_hooks'); + +const storage = new AsyncLocalStorage(); + +// Without using, the scope must be disposed manually +const scope = storage.withScope('my-store'); +// storage.getStore() === 'my-store' here + +scope.dispose(); // Restore previous value +// storage.getStore() === undefined here +``` + ## Class: `AsyncResource`