ECMAScript Async Explicit Resource Management
See the codeECMAScript Async Explicit Resource Management has been subsumed by the ECMAScript Explicit Resource Management Proposal. Further discussion should occur in that repository.
This proposal relates to async functionality deferred from the original Explicit Resource Management proposal and shares the same motivations:
This proposal intends to address a common pattern in software development regarding the lifetime and management of various resources (memory, I/O, etc.). This pattern generally includes the allocation of a resource and the ability to explicitly release critical resources.
Please note: Specification text for this proposal is yet to be updated since this split occurred.
For example, ECMAScript Async Generator Functions expose this pattern through the return method, as a means to
explicitly evaluate finally blocks to ensure user-defined cleanup logic is preserved:
async function * g() {
const stream = acquireStream(); // critical resource
try {
...
}
finally {
await stream.close(); // cleanup
}
}
const obj = g();
try {
const r = await obj.next();
...
}
finally {
await obj.return(); // calls finally blocks in `g`
}
As such, we propose the adoption of a novel syntax to simplify this common pattern:
// in an async function:
async function * g() {
await using handle = acquireFileHandle(); // async-block-scoped critical resource
} // async cleanup
// in a block in an async context:
{
await using obj = g(); // block-scoped declaration
const r = await obj.next();
} // calls finally blocks in `g` and awaits result
In addition, we propose the addition of a disposable container object to assist with managing multiple resources:
AsyncDisposableStack — A stack-based container of asynchronously disposable resources.Stage: 3
Champion: Ron Buckton (@rbuckton)
Last Presented: March, 2023 (slides,
notes #1,
notes #2)
For more information see the TC39 proposal process.
This proposal is motivated by a number of cases:
iterator.return()reader.releaseLock()handle.close()Module._free(ptr) obj.delete() Module.destroy(obj)const writer = getWritableStream();
...
await writer.close(); // Oops, should have been in a try/finally
const writer = ...;
try {
... // ok to use `writer`
}
finally {
await writer.close();
}
// not ok to use `writer`, but still in scope
await-ed:
const writer = ...;
try {
...
}
finally {
writer.close(); // oops, forgot `await`
}
const a = ...;
const b = ...;
try {
...
}
finally {
await a.close(); // Oops, issue if `b.close()` depends on `a`.
await b.close(); // Oops, `b` never reached if `a.close()` throws.
}
{ // block avoids leaking `a` or `b` to outer scope
const a = ...;
try {
const b = ...;
try {
...
}
finally {
await b.close(); // ensure `b` is closed before `a` in case `b`
// depends on `a`
}
}
finally {
await a.close(); // ensure `a` is closed even if `b.close()` throws
}
}
// both `a` and `b` are out of scope
Compared to:
// avoids leaking `a` or `b` to outer scope
// ensures `b` is disposed before `a` in case `b` depends on `a`
// ensures `a` is disposed even if disposing `b` throws
await using a = ..., b = ...;
...
import { AsyncReaderWriterLock } from "...";
const lock = new AsyncReaderWriterLock();
export async function readData() {
// wait for outstanding writer and take a read lock
await using lockHandle = await lock.read();
... // any number of readers
await ...;
... // still in read lock after `await`
} // release the read lock, awaiting the result
export async function writeData(data) {
// wait for all readers and take a write lock
await using lockHandle = await lock.write();
... // only one writer
await ...;
... // still in write lock after `await`
} // release the write lock, awaiting the result
try-with-resources statementwith statementWeakMap keysWeakSet valuesWeakRef valuesFinalizationRegistry entriesSymbol.dispose or Symbol.asyncDispose) or declaratively (through a block-scoped declaration like using or await using).await using Declarations// an asynchronously-disposed, block-scoped resource
await using x = expr1; // resource w/ local binding
await using y = expr2, z = expr4; // multiple resources
An await using declaration can appear in the following contexts:
for-of or for-await-of statement.await using in for-of and for-await-of Statementsfor (await using x of y) ...
for await (await using x of y) ...
You can use a await using declaration in a for-of or for-await-of statement inside of an async context to
explicitly bind each iterated value as an async disposable resource. for-await-of does not implicitly make a non-async
using declaration into an async await using declaration, as the await markers in for-await-of and await using
are explicit indicators for distinct cases: for await only indicates async iteration, while await using only
indicates async disposal. For example:
// sync iteration, sync disposal
for (using x of y) ; // no implicit `await` at end of each iteration
// sync iteration, async disposal
for (await using x of y) ; // implicit `await` at end of each iteration
// async iteration, sync disposal
for await (using x of y) ; // implicit `await` at end of each iteration
// async iteration, async disposal
for await (await using x of y) ; // implicit `await` at end of each iteration
While there is some overlap in that the last three cases introduce some form of implicit await during execution, it
is intended that the presence or absence of the await modifier in a using declaration is an explicit indicator as to
whether we are expecting the iterated value to have an @@asyncDispose method. This distinction is in line with the
behavior of for-of and for-await-of:
const iter = { [Symbol.iterator]() { return [].values(); } };
const asyncIter = { [Symbol.asyncIterator]() { return [].values(); } };
for (const x of iter) ; // ok: `iter` has @@iterator
for (const x of asyncIter) ; // throws: `asyncIter` does not have @@iterator
for await (const x of iter) ; // ok: `iter` has @@iterator (fallback)
for await (const x of asyncIter) ; // ok: `asyncIter` has @@asyncIterator
using and await using have the same distinction:
const res = { [Symbol.dispose]() {} };
const asyncRes = { [Symbol.asyncDispose]() {} };
using x = res; // ok: `res` has @@dispose
using x = asyncRes; // throws: `asyncRes` does not have @@dispose
await using x = res; // ok: `res` has @@dispose (fallback)
await using x = asyncres; // ok: `asyncRes` has @@asyncDispose
This results in a matrix of behaviors based on the presence of each await marker:
const res = { [Symbol.dispose]() {} };
const asyncRes = { [Symbol.asyncDispose]() {} };
const iter = { [Symbol.iterator]() { return [res, asyncRes].values(); } };
const asyncIter = { [Symbol.asyncIterator]() { return [res, asyncRes].values(); } };
for (using x of iter) ;
// sync iteration, sync disposal
// - `iter` has @@iterator: ok
// - `res` has @@dispose: ok
// - `asyncRes` does not have @@dispose: *error*
for (using x of asyncIter) ;
// sync iteration, sync disposal
// - `asyncIter` does not have @@iterator: *error*
for (await using x of iter) ;
// sync iteration, async disposal
// - `iter` has @@iterator: ok
// - `res` has @@dispose (fallback): ok
// - `asyncRes` has @@asyncDispose: ok
for (await using x of asyncIter) ;
// sync iteration, async disposal
// - `asyncIter` does not have @@iterator: error
for await (using x of iter) ;
// async iteration, sync disposal
// - `iter` has @@iterator (fallback): ok
// - `res` has @@dispose: ok
// - `asyncRes` does not have @@dispose: error
for await (using x of asyncIter) ;
// async iteration, sync disposal
// - `asyncIter` has @@asyncIterator: ok
// - `res` has @@dispose: ok
// - `asyncRes` does not have @@dispose: error
for await (await using x of iter) ;
// async iteration, async disposal
// - `iter` has @@iterator (fallback): ok
// - `res` has @@dispose (fallback): ok
// - `asyncRes` does has @@asyncDispose: ok
for await (await using x of asyncIter) ;
// async iteration, async disposal
// - `asyncIter` has @@asyncIterator: ok
// - `res` has @@dispose (fallback): ok
// - `asyncRes` does has @@asyncDispose: ok
Or, in table form:
| Syntax | Iteration | Disposal |
|---|---|---|
for (using x of y) | @@iterator | @@dispose |
for (await using x of y) | @@iterator | @@asyncDispose/@@dispose |
for await (using x of y) | @@asyncIterator/@@iterator | @@dispose |
for await (await using x of y) | @@asyncIterator/@@iterator | @@asyncDispose/@@dispose |
Please refer to the specification text for the most recent version of the grammar.
await using Declarationsawait using Declarations with Explicit Local BindingsUsingDeclaration :
`using` `await` BindingList `;`
LexicalBinding :
BindingIdentifier Initializer
When an await using declaration is parsed with BindingIdentifier Initializer, the bindings created in the
declaration are tracked for disposal at the end of the containing async function body, Block, or Module:
{
... // (1)
await using x = expr1;
... // (2)
}
The above example has similar runtime semantics as the following transposed representation:
{
const $$try = { stack: [], error: undefined, hasError: false };
try {
... // (1)
const x = expr1;
if (x !== null && x !== undefined) {
let $$dispose = x[Symbol.asyncDispose];
if (typeof $$dispose !== "function") {
$$dispose = x[Symbol.dispose];
}
if (typeof $$dispose !== "function") {
throw new TypeError();
}
$$try.stack.push({ value: x, dispose: $$dispose });
}
... // (2)
}
catch ($$error) {
$$try.error = $$error;
$$try.hasError = true;
}
finally {
while ($$try.stack.length) {
const { value: $$expr, dispose: $$dispose } = $$try.stack.pop();
try {
await $$dispose.call($$expr);
}
catch ($$error) {
$$try.error = $$try.hasError ? new SuppressedError($$error, $$try.error) : $$error;
$$try.hasError = true;
}
}
if ($$try.hasError) {
throw $$try.error;
}
}
}
If exceptions are thrown both in the statements following the await using declaration and in the call to
[Symbol.asyncDispose](), all exceptions are reported.
await using Declarations with Multiple ResourcesAn await using declaration can mix multiple explicit bindings in the same declaration:
{
...
await using x = expr1, y = expr2;
...
}
These bindings are again used to perform resource disposal when the Block or Module exits, however in this case each
resource's [Symbol.asyncDispose]() is invoked in the reverse order of their declaration. This is approximately
equivalent to the following:
{
... // (1)
await using x = expr1;
await using y = expr2;
... // (2)
}
Both of the above cases would have similar runtime semantics as the following transposed representation:
{
const $$try = { stack: [], error: undefined, hasError: false };
try {
... // (1)
const x = expr1;
if (x !== null && x !== undefined) {
let $$dispose = x[Symbol.asyncDispose];
if (typeof $$dispose !== "function") {
$$dispose = x[Symbol.dispose];
}
if (typeof $$dispose !== "function") {
throw new TypeError();
}
$$try.stack.push({ value: x, dispose: $$dispose });
}
const y = expr2;
if (y !== null && y !== undefined) {
let $$dispose = y[Symbol.asyncDispose];
if (typeof $$dispose !== "function") {
$$dispose = y[Symbol.dispose];
}
if (typeof $$dispose !== "function") {
throw new TypeError();
}
$$try.stack.push({ value: y, dispose: $$dispose });
}
... // (2)
}
catch ($$error) {
$$try.error = $$error;
$$try.hasError = true;
}
finally {
while ($$try.stack.length) {
const { value: $$expr, dispose: $$dispose } = $$try.stack.pop();
try {
await $$dispose.call($$expr);
}
catch ($$error) {
$$try.error = $$try.hasError ? new SuppressedError($$error, $$try.error) : $$error;
$$try.hasError = true;
}
}
if ($$try.hasError) {
throw $$try.error;
}
}
}
Since we must always ensure that we properly release resources, we must ensure that any abrupt completion that might occur during binding initialization results in evaluation of the cleanup step. When there are multiple declarations in the list, we track each resource in the order they are declared. As a result, we must release these resources in reverse order.
await using Declarations and null or undefined ValuesThis proposal has opted to ignore null and undefined values provided to await using declarations. This is similar
to the behavior of using in the original Explicit Resource Management proposal, which also
allows null and undefined, as well as C#, which also allows null,. One primary reason for this behavior is to
simplify a common case where a resource might be optional, without requiring duplication of work or needless
allocations:
if (isResourceAvailable()) {
await using resource = getResource();
... // (1)
resource.doSomething()
... // (2)
}
else {
// duplicate code path above
... // (1) above
... // (2) above
}
Compared to:
await using resource = isResourceAvailable() ? getResource() : undefined;
... // (1) do some work with or without resource
resource?.doSomething();
... // (2) do some other work with or without resource
await using Declarations and Values Without [Symbol.asyncDispose] or [Symbol.dispose]If a resource does not have a callable [Symbol.asyncDispose] or [Symbol.asyncDispose] member, a TypeError would be thrown immediately when the resource is tracked.
await using Declarations in for-of and for-await-of LoopsA await using declaration may occur in the ForDeclaration of a for-await-of loop:
for await (await using x of iterateResources()) {
// use x
}
In this case, the value bound to x in each iteration will be asynchronously disposed at the end of each iteration.
This will not dispose resources that are not iterated, such as if iteration is terminated early due to return,
break, or throw.
await using declarations may not be used in in the head of a for-of or for-in loop.
await")The await using syntax introduces an implicit async interleaving point (i.e., an implicit await) whenever control
flow exits an async function body, Block, or Module containing a await using declaration. This means that two
statements that currently execute in the same microtask, such as:
async function f() {
{
a();
} // exit block
b(); // same microtask as call to `a()`
}
will instead execute in different microtasks if a await using declaration is introduced:
async function f() {
{
await using x = ...;
a();
} // exit block, implicit `await`
b(); // different microtask from call to `a()`.
}
It is important that such an implicit interleaving point be adequately indicated within the syntax. We believe that
the presence of await using within such a block is an adequate indicator, since it should be fairly easy to recognize
a Block containing a await using statement in well-formated code.
It is also feasible for editors to use features such as syntax highlighting, editor decorations, and inlay hints to further highlight such transitions, without needing to specify additional syntax.
Further discussion around the await using syntax and how it pertains to implicit async interleaving points can be
found in #1.
The following show examples of using this proposal with various APIs, assuming those APIs adopted this proposal.
{
await using stream = new ReadableStream(...);
...
} // 'stream' is canceled and result is awaited
{
await using writable = ...;
writable.write(...);
} // 'writable.end()' is called and its result is awaited
// roll back transaction if either action fails
async function transfer(account1, account2) {
await using tx = transactionManager.startTransaction(account1, account2);
await account1.debit(amount);
await account2.credit(amount);
// mark transaction success if we reach this point
tx.succeeded = true;
} // await transaction commit or rollback
SymbolThis proposal adds the asyncDispose property to the Symbol constructor whose value is the @@asyncDispose internal symbol:
Well-known Symbols
| Specification Name | [[Description]] | Value and Purpose |
|---|---|---|
| @@asyncDispose | "Symbol.asyncDispose" | A method that asynchronosly explicitly disposes of resources held by the object. Called by the semantics of await using declarations and by AsyncDisposableStack objects. |
TypeScript Definition
interface SymbolConstructor {
readonly asyncDispose: unique symbol;
}
%AsyncIteratorPrototype%.@@asyncDispose()We propose to add Symbol.asyncDispose to the built-in %AsyncIteratorPrototype% as if it had the following behavior:
%AsyncIteratorPrototype%[Symbol.asyncDispose] = async function () {
await this.return();
}
AsyncDisposable InterfaceAsyncDisposable InterfaceAn object is async disposable if it conforms to the following interface:
| Property | Value | Requirements |
|---|---|---|
@@asyncDispose | An async function that performs explicit cleanup. | The function should return a Promise. |
TypeScript Definition
interface AsyncDisposable {
/**
* Disposes of resources within this object.
*/
[Symbol.asyncDispose](): Promise<void>;
}
AsyncDisposableStack container objectAsyncDisposableStack is the async version of DisposableStack, introduced in the
Explicit Resource Management proposal and is a
container used to aggregate async disposables, guaranteeing that every disposable resource in the container is disposed
when the respective disposal method is called. If any disposable in the container throws an error during dispose, or
results in a rejected Promise, it would be thrown at the end (possibly wrapped in a SuppressedError if multiple
errors were thrown):
class AsyncDisposableStack {
constructor();
/**
* Gets a value indicating whether the stack has been disposed.
* @returns {boolean}
*/
get disposed();
/**
* Alias for `[Symbol.asyncDispose]()`.
* @returns {Promise<void>}.
*/
disposeAsync();
/**
* Adds a resource to the top of the stack. Has no effect if provided `null` or `undefined`.
* @template {AsyncDisposable | Disposable | null | undefined} T
* @param {T} value - An `AsyncDisposable` or `Disposable` object, `null`, or `undefined`.
* @returns {T} The provided value.
*/
use(value);
/**
* Adds a non-disposable resource and a disposal callback to the top of the stack.
* @template T
* @param {T} value - A resource to be disposed.
* @param {(value: T) => void | Promise<void>} onDisposeAsync - A callback invoked to dispose the provided value.
* @returns {T} The provided value.
*/
adopt(value, onDisposeAsync);
/**
* Adds a disposal callback to the top of the stack.
* @param {() => void | Promise<void>} onDisposeAsync - A callback to evaluate when this object is disposed.
* @returns {void}
*/
defer(onDisposeAsync);
/**
* Moves all resources currently in this stack into a new `AsyncDisposableStack`.
* @returns {AsyncDisposableStack} The new `AsyncDisposableStack`.
*/
move();
/**
* Asynchronously disposes of resources within this object.
* @returns {Promise<void>}
*/
[Symbol.asyncDispose]();
[Symbol.toStringTag];
}
This class provids the following capabilities:
NOTE:
AsyncDisposableStackis inspired by Python'sAsyncExitStack.
The AsyncDisposableStack classe provide the ability to aggregate multiple disposable resources into a single
container. When the AsyncDisposableStack container is disposed, each object in the container is also guaranteed to be
disposed (barring early termination of the program). If any resource throws an error during dispose, or results in a
rejected Promise, that error will be collected and rethrown after all resources are disposed. If there were multiple
errors, they will be wrapped in nested SuppressedError objects.
For example:
const stack = new AsyncDisposableStack();
const resource1 = stack.use(getResource1());
const resource2 = stack.use(getResource2());
const resource3 = stack.use(getResource3());
await stack[Symbol.asyncDispose](); // dispose and await disposal result of resource3, then resource2, then resource1
If all of resource1, resource2 and resource3 were to throw during disposal, this would produce an exception
similar to the following:
new SuppressedError(
/*error*/ exception_from_resource3_disposal,
/*suppressed*/ new SuppressedError(
/*error*/ exception_from_resource2_disposal,
/*suppressed*/ exception_from_resource1_disposal
)
)
The AsyncDisposableStack class also provides the ability to create a disposable resource from a simple callback. This
callback will be executed when the stack's disposal method is executed.
The ability to create a disposable resource from a callback has several benefits:
await using while working with existing resources that do not conform to the
Symbol.asyncDispose mechanic:
{
await using stack = new AsyncDisposableStack();
const stream = stack.adopt(createReadableStream(), async reader => await reader.close());
...
}
defer statement:
async function f() {
await using stack = new AsyncDisposableStack();
stack.defer(async () => await someAsyncCleanupOperaiton());
...
}
A user-defined disposable class might need to allocate and track multiple nested resources that should be asynchronously
disposed when the class instance is disposed. However, properly managing the lifetime of these nested resources during
construction can sometimes be difficult. The move method of AsyncDisposableStack helps to more easily manage
lifetime in these scenarios:
const privateConstructorSentinel = {};
class PluginHost {
#disposed = false;
#disposables;
#channel;
#socket;
/** @private */
constructor(arg) {
if (arg !== privateConstructorSentinel) throw new TypeError("Use PluginHost.create() instead");
}
// NOTE: there's no such thing as an async constructor
static async create() {
const host = new PluginHost(privateConstructorSentinel);
// Create an AsyncDisposableStack that is disposed when the constructor exits.
// If construction succeeds, we move everything out of `stack` and into
// `#disposables` to be disposed later.
await using stack = new AsyncDisposableStack();
// Create an IPC adapter around process.send/process.on("message").
// When disposed, it unsubscribes from process.on("message").
host.#channel = stack.use(new NodeProcessIpcChannelAdapter(process));
// Create a pseudo-websocket that sends and receives messages over
// a NodeJS IPC channel.
host.#socket = stack.use(new NodePluginHostIpcSocket(host.#channel));
// If we made it here, then there were no errors during construction and
// we can safely move the disposables out of `stack` and into `#disposables`.
host.#disposables = stack.move();
// If construction failed, then `stack` would be asynchronously disposed before reaching
// the line above. Event handlers would be removed, allowing `#channel` and
// `#socket` to be GC'd.
return host;
}
loadPlugin(file) {
// A disposable should try to ensure access is consistent with its "disposed" state, though this isn't strictly
// necessary since some disposables could be reusable (i.e., a Connection with an `open()` method, etc.).
if (this.#disposed) throw new ReferenceError("Object is disposed.");
// ...
}
async [Symbol.asyncDispose]() {
if (!this.#disposed) {
this.#disposed = true;
const disposables = this.#disposables;
// NOTE: we can free `#socket` and `#channel` here since they will be disposed by the call to
// `disposables[Symbol.asyncDispose]()`, below. This isn't strictly a requirement for every disposable, but is
// good housekeeping since these objects will no longer be useable.
this.#socket = undefined;
this.#channel = undefined;
this.#disposables = undefined;
// Dispose all resources in `disposables`
await disposables[Symbol.asyncDispose]();
}
}
}
This proposal does not necessarily require immediate support in the HTML DOM specification, as existing APIs can still
be adapted by using DisposableStack or AsyncDisposableStack. However, there are a number of APIs that could benefit
from this proposal and should be considered by the relevant standards bodies. The following is by no means a complete
list, and primarily offers suggestions for consideration. The actual implementation is at the discretion of the relevant
standards bodies.
NOTE: This builds on the list defined in the Explicit Resource Management proposal.
AudioContext — @@asyncDispose() as an alias or wrapper for close().
close() here is asynchronous, but uses the same name as similar synchronous methods on other objects.MediaKeySession — @@asyncDispose() as an alias or wrapper for close().
close() here is asynchronous, but uses the same name as similar synchronous methods on other objects.PaymentRequest — @@asyncDispose() could invoke abort() if the payment is still in the active state.
abort() here is asynchronous, but uses the same name as similar synchronous methods on other objects.PushSubscription — @@asyncDispose() as an alias or wrapper for unsubscribe().ReadableStream — @@asyncDispose() as an alias or wrapper for cancel().ReadableStreamDefaultReader — Either @@dispose() as an alias or wrapper for releaseLock(), or
@@asyncDispose() as a wrapper for cancel() (but probably not both).ServiceWorkerRegistration — @@asyncDispose() as a wrapper for unregister().WritableStream — @@asyncDispose() as an alias or wrapper for close().
close() here is asynchronous, but uses the same name as similar synchronous methods on other objects.WritableStreamDefaultWriter — Either @@dispose() as an alias or wrapper for releaseLock(), or
@@asyncDispose() as a wrapper for close() (but probably not both).A wrapper for x() is a method that invokes x(), but only if the object is in a state
such that calling x() will not throw as a result of repeated evaluation.
A callback-adapting wrapper is a wrapper that adapts a continuation passing-style method
that accepts a callback into a Promise-producing method.
A single-use disposer for x() and y() indicates a newly constructed disposable object
that invokes x() when constructed and y() when disposed the first time (and does nothing if the object is disposed
more than once).
This proposal does not necessarily require immediate support in NodeJS, as existing APIs can still be adapted by using
DisposableStack. However, there are a number of APIs that could benefit from this proposal and should be considered by
the NodeJS maintainers. The following is by no means a complete list, and primarily offers suggestions for
consideration. The actual implementation is at the discretion of the NodeJS maintainers.
NOTE: This builds on the list defined in the Explicit Resource Management proposal.
fs.promises.FileHandle — @@asyncDispose() as an alias or wrapper for close().fs.Dir — @@asyncDispose() as an alias or wrapper for close(), @@dispose() as an alias or wrapper
for closeSync().http.ClientRequest — Either @@dispose() or @@asyncDispose() as an alias or wrapper for destroy().http.Server — @@asyncDispose() as a callback-adapting wrapper for close().http.ServerResponse — @@asyncDispose() as a callback-adapting wrapper for end().http.IncomingMessage — Either @@dispose() or @@asyncDispose() as an alias or wrapper for destroy().http.OutgoingMessage — Either @@dispose() or @@asyncDispose() as an alias or wrapper for destroy().http2.Http2Session — @@asyncDispose() as a callback-adapting wrapper for close().http2.Http2Stream — @@asyncDispose() as a callback-adapting wrapper for close().http2.Http2Server — @@asyncDispose() as a callback-adapting wrapper for close().http2.Http2SecureServer — @@asyncDispose() as a callback-adapting wrapper for close().http2.Http2ServerRequest — Either @@dispose() or @@asyncDispose() as an alias or wrapper for
destroy().http2.Http2ServerResponse — @@asyncDispose() as a callback-adapting wrapper for end().https.Server — @@asyncDispose() as a callback-adapting wrapper for close().stream.Writable — Either @@dispose() or @@asyncDispose() as an alias or wrapper for destroy() or
@@asyncDispose only as a callback-adapting wrapper for end() (depending on whether the disposal behavior
should be to drop immediately or to flush any pending writes).stream.Readable — Either @@dispose() or @@asyncDispose() as an alias or wrapper for destroy().net, readline, tls, udp, and worker_threads.await using declarations, Symbol.asyncDispose, and AsyncDisposableStack remain at Stage 2 as an independent
proposal.The following is a high-level list of tasks to progress through each stage of the TC39 proposal process:
JavaScript
100.0%
ECMAScript Async Explicit Resource Management
See the codeECMAScript Async Explicit Resource Management has been subsumed by the ECMAScript Explicit Resource Management Proposal. Further discussion should occur in that repository.
This proposal relates to async functionality deferred from the original Explicit Resource Management proposal and shares the same motivations:
This proposal intends to address a common pattern in software development regarding the lifetime and management of various resources (memory, I/O, etc.). This pattern generally includes the allocation of a resource and the ability to explicitly release critical resources.
Please note: Specification text for this proposal is yet to be updated since this split occurred.
For example, ECMAScript Async Generator Functions expose this pattern through the return method, as a means to
explicitly evaluate finally blocks to ensure user-defined cleanup logic is preserved:
async function * g() {
const stream = acquireStream(); // critical resource
try {
...
}
finally {
await stream.close(); // cleanup
}
}
const obj = g();
try {
const r = await obj.next();
...
}
finally {
await obj.return(); // calls finally blocks in `g`
}
As such, we propose the adoption of a novel syntax to simplify this common pattern:
// in an async function:
async function * g() {
await using handle = acquireFileHandle(); // async-block-scoped critical resource
} // async cleanup
// in a block in an async context:
{
await using obj = g(); // block-scoped declaration
const r = await obj.next();
} // calls finally blocks in `g` and awaits result
In addition, we propose the addition of a disposable container object to assist with managing multiple resources:
AsyncDisposableStack — A stack-based container of asynchronously disposable resources.Stage: 3
Champion: Ron Buckton (@rbuckton)
Last Presented: March, 2023 (slides,
notes #1,
notes #2)
For more information see the TC39 proposal process.
This proposal is motivated by a number of cases:
iterator.return()reader.releaseLock()handle.close()Module._free(ptr) obj.delete() Module.destroy(obj)const writer = getWritableStream();
...
await writer.close(); // Oops, should have been in a try/finally
const writer = ...;
try {
... // ok to use `writer`
}
finally {
await writer.close();
}
// not ok to use `writer`, but still in scope
await-ed:
const writer = ...;
try {
...
}
finally {
writer.close(); // oops, forgot `await`
}
const a = ...;
const b = ...;
try {
...
}
finally {
await a.close(); // Oops, issue if `b.close()` depends on `a`.
await b.close(); // Oops, `b` never reached if `a.close()` throws.
}
{ // block avoids leaking `a` or `b` to outer scope
const a = ...;
try {
const b = ...;
try {
...
}
finally {
await b.close(); // ensure `b` is closed before `a` in case `b`
// depends on `a`
}
}
finally {
await a.close(); // ensure `a` is closed even if `b.close()` throws
}
}
// both `a` and `b` are out of scope
Compared to:
// avoids leaking `a` or `b` to outer scope
// ensures `b` is disposed before `a` in case `b` depends on `a`
// ensures `a` is disposed even if disposing `b` throws
await using a = ..., b = ...;
...
import { AsyncReaderWriterLock } from "...";
const lock = new AsyncReaderWriterLock();
export async function readData() {
// wait for outstanding writer and take a read lock
await using lockHandle = await lock.read();
... // any number of readers
await ...;
... // still in read lock after `await`
} // release the read lock, awaiting the result
export async function writeData(data) {
// wait for all readers and take a write lock
await using lockHandle = await lock.write();
... // only one writer
await ...;
... // still in write lock after `await`
} // release the write lock, awaiting the result
try-with-resources statementwith statementWeakMap keysWeakSet valuesWeakRef valuesFinalizationRegistry entriesSymbol.dispose or Symbol.asyncDispose) or declaratively (through a block-scoped declaration like using or await using).await using Declarations// an asynchronously-disposed, block-scoped resource
await using x = expr1; // resource w/ local binding
await using y = expr2, z = expr4; // multiple resources
An await using declaration can appear in the following contexts:
for-of or for-await-of statement.await using in for-of and for-await-of Statementsfor (await using x of y) ...
for await (await using x of y) ...
You can use a await using declaration in a for-of or for-await-of statement inside of an async context to
explicitly bind each iterated value as an async disposable resource. for-await-of does not implicitly make a non-async
using declaration into an async await using declaration, as the await markers in for-await-of and await using
are explicit indicators for distinct cases: for await only indicates async iteration, while await using only
indicates async disposal. For example:
// sync iteration, sync disposal
for (using x of y) ; // no implicit `await` at end of each iteration
// sync iteration, async disposal
for (await using x of y) ; // implicit `await` at end of each iteration
// async iteration, sync disposal
for await (using x of y) ; // implicit `await` at end of each iteration
// async iteration, async disposal
for await (await using x of y) ; // implicit `await` at end of each iteration
While there is some overlap in that the last three cases introduce some form of implicit await during execution, it
is intended that the presence or absence of the await modifier in a using declaration is an explicit indicator as to
whether we are expecting the iterated value to have an @@asyncDispose method. This distinction is in line with the
behavior of for-of and for-await-of:
const iter = { [Symbol.iterator]() { return [].values(); } };
const asyncIter = { [Symbol.asyncIterator]() { return [].values(); } };
for (const x of iter) ; // ok: `iter` has @@iterator
for (const x of asyncIter) ; // throws: `asyncIter` does not have @@iterator
for await (const x of iter) ; // ok: `iter` has @@iterator (fallback)
for await (const x of asyncIter) ; // ok: `asyncIter` has @@asyncIterator
using and await using have the same distinction:
const res = { [Symbol.dispose]() {} };
const asyncRes = { [Symbol.asyncDispose]() {} };
using x = res; // ok: `res` has @@dispose
using x = asyncRes; // throws: `asyncRes` does not have @@dispose
await using x = res; // ok: `res` has @@dispose (fallback)
await using x = asyncres; // ok: `asyncRes` has @@asyncDispose
This results in a matrix of behaviors based on the presence of each await marker:
const res = { [Symbol.dispose]() {} };
const asyncRes = { [Symbol.asyncDispose]() {} };
const iter = { [Symbol.iterator]() { return [res, asyncRes].values(); } };
const asyncIter = { [Symbol.asyncIterator]() { return [res, asyncRes].values(); } };
for (using x of iter) ;
// sync iteration, sync disposal
// - `iter` has @@iterator: ok
// - `res` has @@dispose: ok
// - `asyncRes` does not have @@dispose: *error*
for (using x of asyncIter) ;
// sync iteration, sync disposal
// - `asyncIter` does not have @@iterator: *error*
for (await using x of iter) ;
// sync iteration, async disposal
// - `iter` has @@iterator: ok
// - `res` has @@dispose (fallback): ok
// - `asyncRes` has @@asyncDispose: ok
for (await using x of asyncIter) ;
// sync iteration, async disposal
// - `asyncIter` does not have @@iterator: error
for await (using x of iter) ;
// async iteration, sync disposal
// - `iter` has @@iterator (fallback): ok
// - `res` has @@dispose: ok
// - `asyncRes` does not have @@dispose: error
for await (using x of asyncIter) ;
// async iteration, sync disposal
// - `asyncIter` has @@asyncIterator: ok
// - `res` has @@dispose: ok
// - `asyncRes` does not have @@dispose: error
for await (await using x of iter) ;
// async iteration, async disposal
// - `iter` has @@iterator (fallback): ok
// - `res` has @@dispose (fallback): ok
// - `asyncRes` does has @@asyncDispose: ok
for await (await using x of asyncIter) ;
// async iteration, async disposal
// - `asyncIter` has @@asyncIterator: ok
// - `res` has @@dispose (fallback): ok
// - `asyncRes` does has @@asyncDispose: ok
Or, in table form:
| Syntax | Iteration | Disposal |
|---|---|---|
for (using x of y) | @@iterator | @@dispose |
for (await using x of y) | @@iterator | @@asyncDispose/@@dispose |
for await (using x of y) | @@asyncIterator/@@iterator | @@dispose |
for await (await using x of y) | @@asyncIterator/@@iterator | @@asyncDispose/@@dispose |
Please refer to the specification text for the most recent version of the grammar.
await using Declarationsawait using Declarations with Explicit Local BindingsUsingDeclaration :
`using` `await` BindingList `;`
LexicalBinding :
BindingIdentifier Initializer
When an await using declaration is parsed with BindingIdentifier Initializer, the bindings created in the
declaration are tracked for disposal at the end of the containing async function body, Block, or Module:
{
... // (1)
await using x = expr1;
... // (2)
}
The above example has similar runtime semantics as the following transposed representation:
{
const $$try = { stack: [], error: undefined, hasError: false };
try {
... // (1)
const x = expr1;
if (x !== null && x !== undefined) {
let $$dispose = x[Symbol.asyncDispose];
if (typeof $$dispose !== "function") {
$$dispose = x[Symbol.dispose];
}
if (typeof $$dispose !== "function") {
throw new TypeError();
}
$$try.stack.push({ value: x, dispose: $$dispose });
}
... // (2)
}
catch ($$error) {
$$try.error = $$error;
$$try.hasError = true;
}
finally {
while ($$try.stack.length) {
const { value: $$expr, dispose: $$dispose } = $$try.stack.pop();
try {
await $$dispose.call($$expr);
}
catch ($$error) {
$$try.error = $$try.hasError ? new SuppressedError($$error, $$try.error) : $$error;
$$try.hasError = true;
}
}
if ($$try.hasError) {
throw $$try.error;
}
}
}
If exceptions are thrown both in the statements following the await using declaration and in the call to
[Symbol.asyncDispose](), all exceptions are reported.
await using Declarations with Multiple ResourcesAn await using declaration can mix multiple explicit bindings in the same declaration:
{
...
await using x = expr1, y = expr2;
...
}
These bindings are again used to perform resource disposal when the Block or Module exits, however in this case each
resource's [Symbol.asyncDispose]() is invoked in the reverse order of their declaration. This is approximately
equivalent to the following:
{
... // (1)
await using x = expr1;
await using y = expr2;
... // (2)
}
Both of the above cases would have similar runtime semantics as the following transposed representation:
{
const $$try = { stack: [], error: undefined, hasError: false };
try {
... // (1)
const x = expr1;
if (x !== null && x !== undefined) {
let $$dispose = x[Symbol.asyncDispose];
if (typeof $$dispose !== "function") {
$$dispose = x[Symbol.dispose];
}
if (typeof $$dispose !== "function") {
throw new TypeError();
}
$$try.stack.push({ value: x, dispose: $$dispose });
}
const y = expr2;
if (y !== null && y !== undefined) {
let $$dispose = y[Symbol.asyncDispose];
if (typeof $$dispose !== "function") {
$$dispose = y[Symbol.dispose];
}
if (typeof $$dispose !== "function") {
throw new TypeError();
}
$$try.stack.push({ value: y, dispose: $$dispose });
}
... // (2)
}
catch ($$error) {
$$try.error = $$error;
$$try.hasError = true;
}
finally {
while ($$try.stack.length) {
const { value: $$expr, dispose: $$dispose } = $$try.stack.pop();
try {
await $$dispose.call($$expr);
}
catch ($$error) {
$$try.error = $$try.hasError ? new SuppressedError($$error, $$try.error) : $$error;
$$try.hasError = true;
}
}
if ($$try.hasError) {
throw $$try.error;
}
}
}
Since we must always ensure that we properly release resources, we must ensure that any abrupt completion that might occur during binding initialization results in evaluation of the cleanup step. When there are multiple declarations in the list, we track each resource in the order they are declared. As a result, we must release these resources in reverse order.
await using Declarations and null or undefined ValuesThis proposal has opted to ignore null and undefined values provided to await using declarations. This is similar
to the behavior of using in the original Explicit Resource Management proposal, which also
allows null and undefined, as well as C#, which also allows null,. One primary reason for this behavior is to
simplify a common case where a resource might be optional, without requiring duplication of work or needless
allocations:
if (isResourceAvailable()) {
await using resource = getResource();
... // (1)
resource.doSomething()
... // (2)
}
else {
// duplicate code path above
... // (1) above
... // (2) above
}
Compared to:
await using resource = isResourceAvailable() ? getResource() : undefined;
... // (1) do some work with or without resource
resource?.doSomething();
... // (2) do some other work with or without resource
await using Declarations and Values Without [Symbol.asyncDispose] or [Symbol.dispose]If a resource does not have a callable [Symbol.asyncDispose] or [Symbol.asyncDispose] member, a TypeError would be thrown immediately when the resource is tracked.
await using Declarations in for-of and for-await-of LoopsA await using declaration may occur in the ForDeclaration of a for-await-of loop:
for await (await using x of iterateResources()) {
// use x
}
In this case, the value bound to x in each iteration will be asynchronously disposed at the end of each iteration.
This will not dispose resources that are not iterated, such as if iteration is terminated early due to return,
break, or throw.
await using declarations may not be used in in the head of a for-of or for-in loop.
await")The await using syntax introduces an implicit async interleaving point (i.e., an implicit await) whenever control
flow exits an async function body, Block, or Module containing a await using declaration. This means that two
statements that currently execute in the same microtask, such as:
async function f() {
{
a();
} // exit block
b(); // same microtask as call to `a()`
}
will instead execute in different microtasks if a await using declaration is introduced:
async function f() {
{
await using x = ...;
a();
} // exit block, implicit `await`
b(); // different microtask from call to `a()`.
}
It is important that such an implicit interleaving point be adequately indicated within the syntax. We believe that
the presence of await using within such a block is an adequate indicator, since it should be fairly easy to recognize
a Block containing a await using statement in well-formated code.
It is also feasible for editors to use features such as syntax highlighting, editor decorations, and inlay hints to further highlight such transitions, without needing to specify additional syntax.
Further discussion around the await using syntax and how it pertains to implicit async interleaving points can be
found in #1.
The following show examples of using this proposal with various APIs, assuming those APIs adopted this proposal.
{
await using stream = new ReadableStream(...);
...
} // 'stream' is canceled and result is awaited
{
await using writable = ...;
writable.write(...);
} // 'writable.end()' is called and its result is awaited
// roll back transaction if either action fails
async function transfer(account1, account2) {
await using tx = transactionManager.startTransaction(account1, account2);
await account1.debit(amount);
await account2.credit(amount);
// mark transaction success if we reach this point
tx.succeeded = true;
} // await transaction commit or rollback
SymbolThis proposal adds the asyncDispose property to the Symbol constructor whose value is the @@asyncDispose internal symbol:
Well-known Symbols
| Specification Name | [[Description]] | Value and Purpose |
|---|---|---|
| @@asyncDispose | "Symbol.asyncDispose" | A method that asynchronosly explicitly disposes of resources held by the object. Called by the semantics of await using declarations and by AsyncDisposableStack objects. |
TypeScript Definition
interface SymbolConstructor {
readonly asyncDispose: unique symbol;
}
%AsyncIteratorPrototype%.@@asyncDispose()We propose to add Symbol.asyncDispose to the built-in %AsyncIteratorPrototype% as if it had the following behavior:
%AsyncIteratorPrototype%[Symbol.asyncDispose] = async function () {
await this.return();
}
AsyncDisposable InterfaceAsyncDisposable InterfaceAn object is async disposable if it conforms to the following interface:
| Property | Value | Requirements |
|---|---|---|
@@asyncDispose | An async function that performs explicit cleanup. | The function should return a Promise. |
TypeScript Definition
interface AsyncDisposable {
/**
* Disposes of resources within this object.
*/
[Symbol.asyncDispose](): Promise<void>;
}
AsyncDisposableStack container objectAsyncDisposableStack is the async version of DisposableStack, introduced in the
Explicit Resource Management proposal and is a
container used to aggregate async disposables, guaranteeing that every disposable resource in the container is disposed
when the respective disposal method is called. If any disposable in the container throws an error during dispose, or
results in a rejected Promise, it would be thrown at the end (possibly wrapped in a SuppressedError if multiple
errors were thrown):
class AsyncDisposableStack {
constructor();
/**
* Gets a value indicating whether the stack has been disposed.
* @returns {boolean}
*/
get disposed();
/**
* Alias for `[Symbol.asyncDispose]()`.
* @returns {Promise<void>}.
*/
disposeAsync();
/**
* Adds a resource to the top of the stack. Has no effect if provided `null` or `undefined`.
* @template {AsyncDisposable | Disposable | null | undefined} T
* @param {T} value - An `AsyncDisposable` or `Disposable` object, `null`, or `undefined`.
* @returns {T} The provided value.
*/
use(value);
/**
* Adds a non-disposable resource and a disposal callback to the top of the stack.
* @template T
* @param {T} value - A resource to be disposed.
* @param {(value: T) => void | Promise<void>} onDisposeAsync - A callback invoked to dispose the provided value.
* @returns {T} The provided value.
*/
adopt(value, onDisposeAsync);
/**
* Adds a disposal callback to the top of the stack.
* @param {() => void | Promise<void>} onDisposeAsync - A callback to evaluate when this object is disposed.
* @returns {void}
*/
defer(onDisposeAsync);
/**
* Moves all resources currently in this stack into a new `AsyncDisposableStack`.
* @returns {AsyncDisposableStack} The new `AsyncDisposableStack`.
*/
move();
/**
* Asynchronously disposes of resources within this object.
* @returns {Promise<void>}
*/
[Symbol.asyncDispose]();
[Symbol.toStringTag];
}
This class provids the following capabilities:
NOTE:
AsyncDisposableStackis inspired by Python'sAsyncExitStack.
The AsyncDisposableStack classe provide the ability to aggregate multiple disposable resources into a single
container. When the AsyncDisposableStack container is disposed, each object in the container is also guaranteed to be
disposed (barring early termination of the program). If any resource throws an error during dispose, or results in a
rejected Promise, that error will be collected and rethrown after all resources are disposed. If there were multiple
errors, they will be wrapped in nested SuppressedError objects.
For example:
const stack = new AsyncDisposableStack();
const resource1 = stack.use(getResource1());
const resource2 = stack.use(getResource2());
const resource3 = stack.use(getResource3());
await stack[Symbol.asyncDispose](); // dispose and await disposal result of resource3, then resource2, then resource1
If all of resource1, resource2 and resource3 were to throw during disposal, this would produce an exception
similar to the following:
new SuppressedError(
/*error*/ exception_from_resource3_disposal,
/*suppressed*/ new SuppressedError(
/*error*/ exception_from_resource2_disposal,
/*suppressed*/ exception_from_resource1_disposal
)
)
The AsyncDisposableStack class also provides the ability to create a disposable resource from a simple callback. This
callback will be executed when the stack's disposal method is executed.
The ability to create a disposable resource from a callback has several benefits:
await using while working with existing resources that do not conform to the
Symbol.asyncDispose mechanic:
{
await using stack = new AsyncDisposableStack();
const stream = stack.adopt(createReadableStream(), async reader => await reader.close());
...
}
defer statement:
async function f() {
await using stack = new AsyncDisposableStack();
stack.defer(async () => await someAsyncCleanupOperaiton());
...
}
A user-defined disposable class might need to allocate and track multiple nested resources that should be asynchronously
disposed when the class instance is disposed. However, properly managing the lifetime of these nested resources during
construction can sometimes be difficult. The move method of AsyncDisposableStack helps to more easily manage
lifetime in these scenarios:
const privateConstructorSentinel = {};
class PluginHost {
#disposed = false;
#disposables;
#channel;
#socket;
/** @private */
constructor(arg) {
if (arg !== privateConstructorSentinel) throw new TypeError("Use PluginHost.create() instead");
}
// NOTE: there's no such thing as an async constructor
static async create() {
const host = new PluginHost(privateConstructorSentinel);
// Create an AsyncDisposableStack that is disposed when the constructor exits.
// If construction succeeds, we move everything out of `stack` and into
// `#disposables` to be disposed later.
await using stack = new AsyncDisposableStack();
// Create an IPC adapter around process.send/process.on("message").
// When disposed, it unsubscribes from process.on("message").
host.#channel = stack.use(new NodeProcessIpcChannelAdapter(process));
// Create a pseudo-websocket that sends and receives messages over
// a NodeJS IPC channel.
host.#socket = stack.use(new NodePluginHostIpcSocket(host.#channel));
// If we made it here, then there were no errors during construction and
// we can safely move the disposables out of `stack` and into `#disposables`.
host.#disposables = stack.move();
// If construction failed, then `stack` would be asynchronously disposed before reaching
// the line above. Event handlers would be removed, allowing `#channel` and
// `#socket` to be GC'd.
return host;
}
loadPlugin(file) {
// A disposable should try to ensure access is consistent with its "disposed" state, though this isn't strictly
// necessary since some disposables could be reusable (i.e., a Connection with an `open()` method, etc.).
if (this.#disposed) throw new ReferenceError("Object is disposed.");
// ...
}
async [Symbol.asyncDispose]() {
if (!this.#disposed) {
this.#disposed = true;
const disposables = this.#disposables;
// NOTE: we can free `#socket` and `#channel` here since they will be disposed by the call to
// `disposables[Symbol.asyncDispose]()`, below. This isn't strictly a requirement for every disposable, but is
// good housekeeping since these objects will no longer be useable.
this.#socket = undefined;
this.#channel = undefined;
this.#disposables = undefined;
// Dispose all resources in `disposables`
await disposables[Symbol.asyncDispose]();
}
}
}
This proposal does not necessarily require immediate support in the HTML DOM specification, as existing APIs can still
be adapted by using DisposableStack or AsyncDisposableStack. However, there are a number of APIs that could benefit
from this proposal and should be considered by the relevant standards bodies. The following is by no means a complete
list, and primarily offers suggestions for consideration. The actual implementation is at the discretion of the relevant
standards bodies.
NOTE: This builds on the list defined in the Explicit Resource Management proposal.
AudioContext — @@asyncDispose() as an alias or wrapper for close().
close() here is asynchronous, but uses the same name as similar synchronous methods on other objects.MediaKeySession — @@asyncDispose() as an alias or wrapper for close().
close() here is asynchronous, but uses the same name as similar synchronous methods on other objects.PaymentRequest — @@asyncDispose() could invoke abort() if the payment is still in the active state.
abort() here is asynchronous, but uses the same name as similar synchronous methods on other objects.PushSubscription — @@asyncDispose() as an alias or wrapper for unsubscribe().ReadableStream — @@asyncDispose() as an alias or wrapper for cancel().ReadableStreamDefaultReader — Either @@dispose() as an alias or wrapper for releaseLock(), or
@@asyncDispose() as a wrapper for cancel() (but probably not both).ServiceWorkerRegistration — @@asyncDispose() as a wrapper for unregister().WritableStream — @@asyncDispose() as an alias or wrapper for close().
close() here is asynchronous, but uses the same name as similar synchronous methods on other objects.WritableStreamDefaultWriter — Either @@dispose() as an alias or wrapper for releaseLock(), or
@@asyncDispose() as a wrapper for close() (but probably not both).A wrapper for x() is a method that invokes x(), but only if the object is in a state
such that calling x() will not throw as a result of repeated evaluation.
A callback-adapting wrapper is a wrapper that adapts a continuation passing-style method
that accepts a callback into a Promise-producing method.
A single-use disposer for x() and y() indicates a newly constructed disposable object
that invokes x() when constructed and y() when disposed the first time (and does nothing if the object is disposed
more than once).
This proposal does not necessarily require immediate support in NodeJS, as existing APIs can still be adapted by using
DisposableStack. However, there are a number of APIs that could benefit from this proposal and should be considered by
the NodeJS maintainers. The following is by no means a complete list, and primarily offers suggestions for
consideration. The actual implementation is at the discretion of the NodeJS maintainers.
NOTE: This builds on the list defined in the Explicit Resource Management proposal.
fs.promises.FileHandle — @@asyncDispose() as an alias or wrapper for close().fs.Dir — @@asyncDispose() as an alias or wrapper for close(), @@dispose() as an alias or wrapper
for closeSync().http.ClientRequest — Either @@dispose() or @@asyncDispose() as an alias or wrapper for destroy().http.Server — @@asyncDispose() as a callback-adapting wrapper for close().http.ServerResponse — @@asyncDispose() as a callback-adapting wrapper for end().http.IncomingMessage — Either @@dispose() or @@asyncDispose() as an alias or wrapper for destroy().http.OutgoingMessage — Either @@dispose() or @@asyncDispose() as an alias or wrapper for destroy().http2.Http2Session — @@asyncDispose() as a callback-adapting wrapper for close().http2.Http2Stream — @@asyncDispose() as a callback-adapting wrapper for close().http2.Http2Server — @@asyncDispose() as a callback-adapting wrapper for close().http2.Http2SecureServer — @@asyncDispose() as a callback-adapting wrapper for close().http2.Http2ServerRequest — Either @@dispose() or @@asyncDispose() as an alias or wrapper for
destroy().http2.Http2ServerResponse — @@asyncDispose() as a callback-adapting wrapper for end().https.Server — @@asyncDispose() as a callback-adapting wrapper for close().stream.Writable — Either @@dispose() or @@asyncDispose() as an alias or wrapper for destroy() or
@@asyncDispose only as a callback-adapting wrapper for end() (depending on whether the disposal behavior
should be to drop immediately or to flush any pending writes).stream.Readable — Either @@dispose() or @@asyncDispose() as an alias or wrapper for destroy().net, readline, tls, udp, and worker_threads.await using declarations, Symbol.asyncDispose, and AsyncDisposableStack remain at Stage 2 as an independent
proposal.The following is a high-level list of tasks to progress through each stage of the TC39 proposal process:
JavaScript
100.0%