Skip to content

async_hooks: add SymbolDispose support to AsyncLocalStorage

This introduces explicit resource management support to AsyncLocalStorage. In order to avoid unexpected leakage of an async local storage value to outer scope, asyncLocalStorage.run requires a callback to nest all sub-procedures in a new function scope. However, when using AsyncLocalStorage with different function types, like async generator, moving statements and expression to a new function body could be evaluated in different ways.

For example, given an async generator:

class C {
  async* foo() {
    await a();
    yield b();
    await c();
  }
}

Then, if we want to modify the async local storage value for b() and c(), simply moving statements would not work:

const storage = new AsyncLocalStorage();

class C {
  async* foo() {
    await a();
    storage.run(value, () => {
      yield b(); // syntax error, arrow function is not a generator
      await this.c(); // syntax error, arrow function is not async
    });
  }
}

Making the arrow function to be an async generator as well still requires significant refactoring:

const storage = new AsyncLocalStorage();

class C {
  async* foo() {
    await a();
    yield* storage.run(value, async function*() => {
      yield b(); // ok
      await this.c(); // reference error, this is undefined
    });
  }
}

This could be potentially more convenient with using declarations:

const storage = new AsyncLocalStorage();
class C {
  async* foo() {
    await a();
    using _ = storage.disposableStore(value);
    yield b();
    await this.c();
  }
}

However, creating a disposable without a using declaration could still be a problem leading to leakage of the async local storage value. To help identifying such mis-use, an ERR_ASYNC_LOCAL_STORAGE_NOT_DISPOSED error is thrown if an AsyncLocalStorageDisposableStore instance is not disposed at the end of the current async resource scope.

Merge request reports

Loading