Skip to content

async_hooks: add using scopes to AsyncLocalStorage#61674

Open
Qard wants to merge 1 commit intonodejs:mainfrom
Qard:als-run-scope
Open

async_hooks: add using scopes to AsyncLocalStorage#61674
Qard wants to merge 1 commit intonodejs:mainfrom
Qard:als-run-scope

Conversation

@Qard
Copy link
Member

@Qard Qard commented Feb 4, 2026

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.

cc @nodejs/diagnostics

@Qard Qard self-assigned this Feb 4, 2026
@Qard Qard added async_hooks Issues and PRs related to the async hooks subsystem. async_local_storage AsyncLocalStorage labels Feb 4, 2026
@nodejs-github-bot nodejs-github-bot added the needs-ci PRs that need a full CI run. label Feb 4, 2026
@Qard Qard force-pushed the als-run-scope branch 2 times, most recently from af2ad3e to d8e83aa Compare February 4, 2026 11:50
@Flarna
Copy link
Member

Flarna commented Feb 4, 2026

I guess this replaces #58104 right?

@Qard
Copy link
Member Author

Qard commented Feb 4, 2026

Ah, yes. Forgot that one existed. 😅

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.
@Qard Qard added the request-ci Add this label to start a Jenkins CI on a PR. label Feb 4, 2026
@github-actions github-actions bot added request-ci-failed An error occurred while starting CI via request-ci label, and manual interventon is needed. and removed request-ci Add this label to start a Jenkins CI on a PR. labels Feb 4, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 4, 2026

Failed to start CI
   ⚠  No approving reviews found
   ✘  Refusing to run CI on potentially unsafe PR
https://github.com/nodejs/node/actions/runs/21678728231

@codecov
Copy link

codecov bot commented Feb 4, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.75%. Comparing base (5e818c9) to head (09cb6ad).
⚠️ Report is 13 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #61674      +/-   ##
==========================================
+ Coverage   89.74%   89.75%   +0.01%     
==========================================
  Files         674      675       +1     
  Lines      204389   204428      +39     
  Branches    39280    39284       +4     
==========================================
+ Hits       183424   183480      +56     
- Misses      13264    13265       +1     
+ Partials     7701     7683      -18     
Files with missing lines Coverage Δ
...nternal/async_local_storage/async_context_frame.js 100.00% <100.00%> (ø)
lib/internal/async_local_storage/async_hooks.js 98.03% <100.00%> (+0.08%) ⬆️
lib/internal/async_local_storage/run_scope.js 100.00% <100.00%> (ø)

... and 28 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@addaleax addaleax added semver-minor PRs that contain new features and should be released in the next minor version. request-ci Add this label to start a Jenkins CI on a PR. author ready PRs that have at least one approval, no pending requests for changes, and a CI started. and removed request-ci-failed An error occurred while starting CI via request-ci label, and manual interventon is needed. labels Feb 6, 2026
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Feb 6, 2026
@nodejs-github-bot
Copy link
Collaborator

@nodejs-github-bot
Copy link
Collaborator

scope[Symbol.dispose]();
assert.strictEqual(storage.getStore(), undefined);

// Double dispose should be idempotent
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe adapt to following to better verify that second call does nothing:

  storage.enterWith('test2');
  assert.strictEqual(storage.getStore(), 'test2');

  // Double dispose should be idempotent
  scope[Symbol.dispose]();
  assert.strictEqual(storage.getStore(), 'test2');

const asyncLocalStorage = new AsyncLocalStorage();

{
using scope = asyncLocalStorage.withScope('my-store');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
using scope = asyncLocalStorage.withScope('my-store');
using _ = asyncLocalStorage.withScope('my-store');

I think this is the usual guideline if unused. Feel free to ignore.

const asyncLocalStorage = new AsyncLocalStorage();

{
using scope = asyncLocalStorage.withScope('my-store');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
using scope = asyncLocalStorage.withScope('my-store');
using _ = asyncLocalStorage.withScope('my-store');

const asyncLocalStorage = new AsyncLocalStorage();

try {
using scope = asyncLocalStorage.withScope('my-store');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
using scope = asyncLocalStorage.withScope('my-store');
using _ = asyncLocalStorage.withScope('my-store');

const asyncLocalStorage = new AsyncLocalStorage();

try {
using scope = asyncLocalStorage.withScope('my-store');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
using scope = asyncLocalStorage.withScope('my-store');
using _ = asyncLocalStorage.withScope('my-store');


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).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we clarify (and test) that restore previous also undoes any enterWith done inside a scope?
e.g.

als.enterWith("store1");
console.log(als.getStore()); // store1
{
    using _ = als.withScope("store2");
    console.log(als.getStore()); // store2
    als.enterWith("store3");
    console.log(als.getStore()); // store3
}
console.log(als.getStore()); // store1

It's quite obvious here but if enterWith() is somewhere in a called function and intended to "leak" it's not so clear.
On the other hand enterWith() (in special a leaking one) shouldn't be used anyway..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's actually kind of a feature that this will help prevent leaking enterWith() calls from going too far. 😅

But yes, I'll find some time to take another pass at this soon and address your feedback. 🙂

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

Labels

async_hooks Issues and PRs related to the async hooks subsystem. async_local_storage AsyncLocalStorage author ready PRs that have at least one approval, no pending requests for changes, and a CI started. needs-ci PRs that need a full CI run. semver-minor PRs that contain new features and should be released in the next minor version.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants