@@ -386,6 +386,110 @@ try { |
386 | 386 | } |
387 | 387 | ``` |
388 | 388 |
|
| 389 | +### `asyncLocalStorage.withScope(store)` |
| 390 | + |
| 391 | +<!-- YAML |
| 392 | +added: REPLACEME |
| 393 | +--> |
| 394 | + |
| 395 | +> Stability: 1 - Experimental |
| 396 | +
|
| 397 | +* `store` {any} |
| 398 | +* Returns: {RunScope} |
| 399 | + |
| 400 | +Creates a disposable scope that enters the given store and automatically |
| 401 | +restores the previous store value when the scope is disposed. This method is |
| 402 | +designed to work with JavaScript's explicit resource management (`using` syntax). |
| 403 | + |
| 404 | +Example: |
| 405 | + |
| 406 | +```mjs |
| 407 | +import { AsyncLocalStorage } from 'node:async_hooks'; |
| 408 | + |
| 409 | +const asyncLocalStorage = new AsyncLocalStorage(); |
| 410 | + |
| 411 | +{ |
| 412 | + using _ = asyncLocalStorage.withScope('my-store'); |
| 413 | + console.log(asyncLocalStorage.getStore()); // Prints: my-store |
| 414 | +} |
| 415 | + |
| 416 | +console.log(asyncLocalStorage.getStore()); // Prints: undefined |
| 417 | +``` |
| 418 | + |
| 419 | +```cjs |
| 420 | +const { AsyncLocalStorage } = require('node:async_hooks'); |
| 421 | + |
| 422 | +const asyncLocalStorage = new AsyncLocalStorage(); |
| 423 | + |
| 424 | +{ |
| 425 | + using _ = asyncLocalStorage.withScope('my-store'); |
| 426 | + console.log(asyncLocalStorage.getStore()); // Prints: my-store |
| 427 | +} |
| 428 | + |
| 429 | +console.log(asyncLocalStorage.getStore()); // Prints: undefined |
| 430 | +``` |
| 431 | + |
| 432 | +The `withScope()` method is particularly useful for managing context in |
| 433 | +synchronous code where you want to ensure the previous store value is restored |
| 434 | +when exiting a block, even if an error is thrown. |
| 435 | + |
| 436 | +```mjs |
| 437 | +import { AsyncLocalStorage } from 'node:async_hooks'; |
| 438 | + |
| 439 | +const asyncLocalStorage = new AsyncLocalStorage(); |
| 440 | + |
| 441 | +try { |
| 442 | + using _ = asyncLocalStorage.withScope('my-store'); |
| 443 | + console.log(asyncLocalStorage.getStore()); // Prints: my-store |
| 444 | + throw new Error('test'); |
| 445 | +} catch (e) { |
| 446 | + // Store is automatically restored even after error |
| 447 | + console.log(asyncLocalStorage.getStore()); // Prints: undefined |
| 448 | +} |
| 449 | +``` |
| 450 | + |
| 451 | +```cjs |
| 452 | +const { AsyncLocalStorage } = require('node:async_hooks'); |
| 453 | + |
| 454 | +const asyncLocalStorage = new AsyncLocalStorage(); |
| 455 | + |
| 456 | +try { |
| 457 | + using _ = asyncLocalStorage.withScope('my-store'); |
| 458 | + console.log(asyncLocalStorage.getStore()); // Prints: my-store |
| 459 | + throw new Error('test'); |
| 460 | +} catch (e) { |
| 461 | + // Store is automatically restored even after error |
| 462 | + console.log(asyncLocalStorage.getStore()); // Prints: undefined |
| 463 | +} |
| 464 | +``` |
| 465 | + |
| 466 | +**Important:** When using `withScope()` in async functions before the first |
| 467 | +`await`, be aware that the scope change will affect the caller's context. The |
| 468 | +synchronous portion of an async function (before the first `await`) runs |
| 469 | +immediately when called, and when it reaches the first `await`, it returns the |
| 470 | +promise to the caller. At that point, the scope change becomes visible in the |
| 471 | +caller's context and will persist in subsequent synchronous code until something |
| 472 | +else changes the scope value. For async operations, prefer using `run()` which |
| 473 | +properly isolates context across async boundaries. |
| 474 | + |
| 475 | +```mjs |
| 476 | +import { AsyncLocalStorage } from 'node:async_hooks'; |
| 477 | + |
| 478 | +const asyncLocalStorage = new AsyncLocalStorage(); |
| 479 | + |
| 480 | +async function example() { |
| 481 | + using _ = asyncLocalStorage.withScope('my-store'); |
| 482 | + console.log(asyncLocalStorage.getStore()); // Prints: my-store |
| 483 | + await someAsyncOperation(); // Function pauses here and returns promise |
| 484 | + console.log(asyncLocalStorage.getStore()); // Prints: my-store |
| 485 | +} |
| 486 | + |
| 487 | +// Calling without await |
| 488 | +example(); // Synchronous portion runs, then pauses at first await |
| 489 | +// After the promise is returned, the scope 'my-store' is now active in caller! |
| 490 | +console.log(asyncLocalStorage.getStore()); // Prints: my-store (unexpected!) |
| 491 | +``` |
| 492 | + |
389 | 493 | ### Usage with `async/await` |
390 | 494 |
|
391 | 495 | 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 |
420 | 524 | for the loss. When the code logs `undefined`, the last callback called is |
421 | 525 | probably responsible for the context loss. |
422 | 526 |
|
| 527 | +## Class: `RunScope` |
| 528 | + |
| 529 | +<!-- YAML |
| 530 | +added: REPLACEME |
| 531 | +--> |
| 532 | + |
| 533 | +> Stability: 1 - Experimental |
| 534 | +
|
| 535 | +A disposable scope returned by [`asyncLocalStorage.withScope()`][] that |
| 536 | +automatically restores the previous store value when disposed. This class |
| 537 | +implements the [Explicit Resource Management][] protocol and is designed to work |
| 538 | +with JavaScript's `using` syntax. |
| 539 | + |
| 540 | +The scope automatically restores the previous store value when the `using` block |
| 541 | +exits, whether through normal completion or by throwing an error. |
| 542 | + |
| 543 | +### `scope.dispose()` |
| 544 | + |
| 545 | +<!-- YAML |
| 546 | +added: REPLACEME |
| 547 | +--> |
| 548 | + |
| 549 | +Explicitly ends the scope and restores the previous store value. This method |
| 550 | +is idempotent: calling it multiple times has the same effect as calling it once. |
| 551 | + |
| 552 | +The `[Symbol.dispose]()` method defers to `dispose()`. |
| 553 | + |
| 554 | +If `withScope()` is called without the `using` keyword, `dispose()` must be |
| 555 | +called manually to restore the previous store value. Forgetting to call |
| 556 | +`dispose()` will cause the store value to persist for the remainder of the |
| 557 | +current execution context: |
| 558 | + |
| 559 | +```mjs |
| 560 | +import { AsyncLocalStorage } from 'node:async_hooks'; |
| 561 | + |
| 562 | +const storage = new AsyncLocalStorage(); |
| 563 | + |
| 564 | +// Without using, the scope must be disposed manually |
| 565 | +const scope = storage.withScope('my-store'); |
| 566 | +// storage.getStore() === 'my-store' here |
| 567 | + |
| 568 | +scope.dispose(); // Restore previous value |
| 569 | +// storage.getStore() === undefined here |
| 570 | +``` |
| 571 | + |
| 572 | +```cjs |
| 573 | +const { AsyncLocalStorage } = require('node:async_hooks'); |
| 574 | + |
| 575 | +const storage = new AsyncLocalStorage(); |
| 576 | + |
| 577 | +// Without using, the scope must be disposed manually |
| 578 | +const scope = storage.withScope('my-store'); |
| 579 | +// storage.getStore() === 'my-store' here |
| 580 | + |
| 581 | +scope.dispose(); // Restore previous value |
| 582 | +// storage.getStore() === undefined here |
| 583 | +``` |
| 584 | + |
423 | 585 | ## Class: `AsyncResource` |
424 | 586 |
|
425 | 587 | <!-- YAML |
@@ -905,8 +1067,10 @@ const server = createServer((req, res) => { |
905 | 1067 | }).listen(3000); |
906 | 1068 | ``` |
907 | 1069 |
|
| 1070 | +[Explicit Resource Management]: https://github.com/tc39/proposal-explicit-resource-management |
908 | 1071 | [`AsyncResource`]: #class-asyncresource |
909 | 1072 | [`EventEmitter`]: events.md#class-eventemitter |
910 | 1073 | [`Stream`]: stream.md#stream |
911 | 1074 | [`Worker`]: worker_threads.md#class-worker |
| 1075 | +[`asyncLocalStorage.withScope()`]: #asynclocalstoragewithscopestore |
912 | 1076 | [`util.promisify()`]: util.md#utilpromisifyoriginal |