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.
For example, ECMAScript 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:
function * g() {
const handle = acquireFileHandle(); // critical resource
try {
...
}
finally {
handle.release(); // cleanup
}
}
const obj = g();
try {
const r = obj.next();
...
}
finally {
obj.return(); // calls finally blocks in `g`
}
As such, we propose the adoption of a novel syntax to simplify this common pattern:
function * g() {
using handle = acquireFileHandle(); // block-scoped critical resource
} // cleanup
{
using obj = g(); // block-scoped declaration
const r = obj.next();
} // calls finally blocks in `g`
In addition, we propose the addition of two disposable container objects to assist with managing multiple resources:
DisposableStack
— A stack-based container of disposable resources.AsyncDisposableStack
— A stack-based container of asynchronously disposable resources.AsyncDisposableStack
has been deferred to a follow-on proposal.
Stage: 3
Champion: Ron Buckton (@rbuckton)
Last Presented: November/December, 2022 (slides,
notes TBA)
For more information see the TC39 proposal process.
- Ron Buckton (@rbuckton)
This proposal is motivated by a number of cases:
-
Inconsistent patterns for resource management:
- ECMAScript Iterators:
iterator.return()
- WHATWG Stream Readers:
reader.releaseLock()
- NodeJS FileHandles:
handle.close()
- Emscripten C++ objects handles:
Module._free(ptr) obj.delete() Module.destroy(obj)
- ECMAScript Iterators:
-
Avoiding common footguns when managing resources:
const reader = stream.getReader(); ... reader.releaseLock(); // Oops, should have been in a try/finally
-
Scoping resources:
const handle = ...; try { ... // ok to use `handle` } finally { handle.close(); } // not ok to use `handle`, but still in scope
-
Avoiding common footguns when managing multiple resources:
const a = ...; const b = ...; try { ... } finally { a.close(); // Oops, issue if `b.close()` depends on `a`. b.close(); // Oops, `b` never reached if `a.close()` throws. }
-
Avoiding lengthy code when managing multiple resources correctly:
{ // block avoids leaking `a` or `b` to outer scope const a = ...; try { const b = ...; try { ... } finally { b.close(); // ensure `b` is closed before `a` in case `b` // depends on `a` } } finally { 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 using a = ..., b = ...; ...
-
Non-blocking memory/IO applications:
import { ReaderWriterLock } from "..."; const lock = new ReaderWriterLock(); export async function readData() { // wait for outstanding writer and take a read lock using lockHandle = await lock.read(); ... // any number of readers await ...; ... // still in read lock after `await` } // release the read lock export async function writeData(data) { // wait for all readers and take a write lock using lockHandle = await lock.write(); ... // only one writer await ...; ... // still in write lock after `await` } // release the write lock
-
Potential for use with the Fixed Layout Objects Proposal and
shared struct
:// main.js shared struct class SharedData { ready = false; processed = false; } const worker = new Worker('worker.js'); const m = new Atomics.Mutex(); const cv = new Atomics.ConditionVariable(); const data = new SharedData(); worker.postMessage({ m, cv, data }); // send data to worker { // wait until main can get a lock on 'm' using lck = m.lock(); // mark data for worker data.ready = true; console.log("main is ready"); } // unlocks 'm' // notify potentially waiting worker cv.notifyOne(); { // reacquire lock on 'm' using lck = m.lock(); // release the lock on 'm' and wait for the worker to finish processing cv.wait(m, () => data.processed); } // unlocks 'm'
// worker.js onmessage = function (e) { const { m, cv, data } = e.data; { // wait until worker can get a lock on 'm' using lck = m.lock(); // release the lock on 'm' and wait until main() sends data cv.wait(m, () => data.ready); // after waiting we once again own the lock on 'm' console.log("worker thread is processing data"); // send data back to main data.processed = true; console.log("worker thread is done"); } // unlocks 'm' }
- C#:
- Java:
try
-with-resources statement - Python:
with
statement
- Resource — An object with a specific lifetime, at the end of which either a lifetime-sensitive operation should be performed or a non-gargbage-collected reference (such as a file handle, socket, etc.) should be closed or freed.
- Resource Management — A process whereby "resources" are released, triggering any lifetime-sensitive operations or freeing any related non-garbage-collected references.
- Implicit Resource Management — Indicates a system whereby the lifetime of a "resource" is managed implicitly
by the runtime as part of garbage collection, such as:
WeakMap
keysWeakSet
valuesWeakRef
valuesFinalizationRegistry
entries
- Explicit Resource Management — Indicates a system whereby the lifetime of a "resource" is managed explicitly
by the user either imperatively (by directly calling a method like
Symbol.dispose
) or declaratively (through a block-scoped declaration likeusing
).
// a synchronously-disposed, block-scoped resource
using x = expr1; // resource w/ local binding
using y = expr2, z = expr4; // multiple resources
Please refer to the specification text for the most recent version of the grammar.
UsingDeclaration :
`using` BindingList `;`
LexicalBinding :
BindingIdentifier Initializer
When a using
declaration is parsed with BindingIdentifier Initializer, the bindings created in the declaration
are tracked for disposal at the end of the containing Block or Module (a using
declaration cannot be used
at the top level of a Script):
{
... // (1)
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) {
const $$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 {
$$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 block following the using
declaration and in the call to
[Symbol.dispose]()
, all exceptions are reported.
A using
declaration can mix multiple explicit bindings in the same declaration:
{
...
using x = expr1, y = expr2;
...
}
These bindings are again used to perform resource disposal when the Block or Module exits, however in this case
[Symbol.dispose]()
is invoked in the reverse order of their declaration. This is approximately equivalent to the
following:
{
... // (1)
using x = expr1;
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) {
const $$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) {
const $$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 {
$$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.
This proposal has opted to ignore null
and undefined
values provided to the using
declarations. This is similar to
the behavior of using
in 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()) {
using resource = getResource();
... // (1)
resource.doSomething()
... // (2)
}
else {
// duplicate code path above
... // (1) above
... // (2) above
}
Compared to:
using resource = isResourceAvailable() ? getResource() : undefined;
... // (1) do some work with or without resource
resource?.doSomething();
... // (2) do some other work with or without resource
If a resource does not have a callable [Symbol.dispose]
member, a TypeError
would be thrown immediately when the
resource is tracked.
A using
declaration may occur in the ForDeclaration of a for-of
or for-await-of
loop:
for (using x of iterateResources()) {
// use x
}
In this case, the value bound to x
in each iteration will be synchronously 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
.
using
declarations may not be used in in the head of a for-in
loop.
The following show examples of using this proposal with various APIs, assuming those APIs adopted this proposal.
{
using reader = stream.getReader();
const { value, done } = reader.read();
} // 'reader' is disposed
{
using f1 = await fs.promises.open(s1, constants.O_RDONLY),
f2 = await fs.promises.open(s2, constants.O_WRONLY);
const buffer = Buffer.alloc(4092);
const { bytesRead } = await f1.read(buffer);
await f2.write(buffer, 0, bytesRead);
} // 'f2' is disposed, then 'f1' is disposed
// audit privileged function call entry and exit
function privilegedActivity() {
using activity = auditLog.startActivity("privilegedActivity"); // log activity start
...
} // log activity end
import { Semaphore } from "...";
const sem = new Semaphore(1); // allow one participant at a time
export async function tryUpdate(record) {
using lck = await sem.wait(); // asynchronously block until we are the sole participant
...
} // synchronously release semaphore and notify the next participant
main_thread.js
// main_thread.js
shared struct Data {
mut;
cv;
ready = 0;
processed = 0;
// ...
}
const data = Data();
data.mut = Atomics.Mutex();
data.cv = Atomics.ConditionVariable();
// start two workers
startWorker1(data);
startWorker2(data);
worker1.js
const data = ...;
const { mut, cv } = data;
{
// lock mutex
using lck = Atomics.Mutex.lock(mut);
// NOTE: at this point we currently own the lock
// load content into data and signal we're ready
// ...
Atomics.store(data, "ready", 1);
} // release mutex
// NOTE: at this point we no longer own the lock
// notify worker 2 that it should wake
Atomics.ConditionVariable.notifyOne(cv);
{
// reacquire lock on mutex
using lck = Atomics.Mutex.lock(mut);
// NOTE: at this point we currently own the lock
// release mutex and wait until condition is met to reacquire it
Atomics.ConditionVariable.wait(mut, () => Atomics.load(data, "processed") === 1);
// NOTE: at this point we currently own the lock
// Do something with the processed data
// ...
} // release mutex
// NOTE: at this point we no longer own the lock
worker2.js
const data = ...;
const { mut, cv } = data;
{
// lock mutex
using lck = Atomics.Mutex.lock(mut);
// NOTE: at this point we currently own the lock
// release mutex and wait until condition is met to reacquire it
Atomics.ConditionVariable.wait(mut, () => Atomics.load(data, "ready") === 1);
// NOTE: at this point we currently own the lock
// read in values from data, perform our processing, then indicate we are done
// ...
Atomics.store(data, "processed", 1);
} // release mutex
// NOTE: at this point we no longer own the lock
This proposal adds the dispose
property to the Symbol
constructor, whose value is the @@dispose
internal symbol:
Well-known Symbols
Specification Name | [[Description]] | Value and Purpose |
---|---|---|
@@dispose | "Symbol.dispose" | A method that explicitly disposes of resources held by the object. Called by the semantics of using declarations and by DisposableStack objects. |
TypeScript Definition
interface SymbolConstructor {
readonly dispose: unique symbol;
}
Even though this proposal no longer includes novel syntax for async disposal, we still define
Symbol.asyncDispose
. Async resource management is extremely valuable even without novel syntax, and
Symbol.asyncDispose
is still necessary to support the semantics of AsyncDisposableStack
. It is our hope that
a follow-on proposal for novel syntax will be adopted by the committee at a future date.
NOTE:
Symbol.asyncDispose
has been moved to a follow-on proposal, per consensus in the November/December, 2022 plenary.
If an exception occurs during resource disposal, it is possible that it might suppress an existing exception thrown
from the body, or from the disposal of another resource. Languages like Java allow you to access a suppressed exception
via a getSuppressed()
method on
the exception. However, ECMAScript allows you to throw any value, not just Error
, so there is no convenient place to
attach a suppressed exception. To better surface these suppressed exceptions and support both logging and error
recovery, this proposal seeks to introduce a new SuppressedError
built-in Error
subclass which would contain both
the error that was most recently thrown, as well as the error that was suppressed:
class SuppressedError extends Error {
/**
* Wraps an error that suppresses another error, and the error that was suppressed.
* @param {*} error The error that resulted in a suppression.
* @param {*} suppressed The error that was suppressed.
* @param {string} message The message for the error.
* @param {{ cause?: * }} [options] Options for the error.
*/
constructor(error, suppressed, message, options);
/**
* The name of the error (i.e., `"SuppressedError"`).
* @type {string}
*/
name = "SuppressedError";
/**
* The error that resulted in a suppression.
* @type {*}
*/
error;
/**
* The error that was suppressed.
* @type {*}
*/
suppressed;
/**
* The message for the error.
* @type {*}
*/
message;
}
We've chosen to use SuppressedError
over AggregateError
for several reasons:
AggregateError
is designed to hold a list of multiple errors, with no correlation between those errors, whileSuppressedError
is intended to hold references to two errors with a direct correlation.AggregateError
is intended to ideally hold a flat list of errors.SuppressedError
is intended to hold a jagged set of errors (i.e.,e.suppressed.suppressed.suppressed
if there were successive error suppressions).- The only error correlation on
AggregateError
is throughcause
, however aSuppressedError
isn't "caused" by the error it suppresses. In addition,cause
is intended to be optional, while theerror
of aSuppressedError
must always be defined.
We also propose to add Symbol.dispose
to the built-in %IteratorPrototype%
as if it had the following behavior:
%IteratorPrototype%[Symbol.dispose] = function () {
this.return();
}
We could also consider adding Symbol.dispose
to such objects as the return value from Proxy.revocable()
, but that
is currently out of scope for the current proposal.
An object is disposable if it conforms to the following interface:
Property | Value | Requirements |
---|---|---|
@@dispose |
A function that performs explicit cleanup. | The function should return undefined . |
TypeScript Definition
interface Disposable {
/**
* Disposes of resources within this object.
*/
[Symbol.dispose](): void;
}
NOTE: The
AsyncDisposable
interface has been moved to a follow-on proposal, per consensus in the November/December, 2022 plenary.
This proposal adds a global object that can act as a container to aggregate 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, it would be thrown at the end (possibly wrapped in a SuppressedError
if
multiple errors were thrown):
class DisposableStack {
constructor();
/**
* Gets a value indicating whether the stack has been disposed.
* @returns {boolean}
*/
get disposed();
/**
* Alias for `[Symbol.dispose]()`.
*/
dispose();
/**
* Adds a resource to the top of the stack. Has no effect if provided `null` or `undefined`.
* @template {Disposable | null | undefined} T
* @param {T} value - A `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} onDispose - A callback invoked to dispose the provided value.
* @returns {T} The provided value.
*/
adopt(value, onDispose);
/**
* Adds a disposal callback to the top of the stack.
* @param {() => void} onDispose - A callback to evaluate when this object is disposed.
* @returns {void}
*/
defer(onDispose);
/**
* Moves all resources currently in this stack into a new `DisposableStack`.
* @returns {DisposableStack} The new `DisposableStack`.
*/
move();
/**
* Disposes of resources within this object.
* @returns {void}
*/
[Symbol.dispose]();
[Symbol.toStringTag];
}
These classes provided the following capabilities:
- Aggregation
- Interoperation and customization
- Assist in complex construction
NOTE:
DisposableStack
is inspired by Python'sExitStack
.
NOTE:
AsyncDisposableStack
has been moved to a follow-on proposal, per consensus in the November/December, 2022 plenary.
The DisposableStack
and class provides the ability to aggregate multiple disposable resources
into a single container. When the AsyncDisposableStack
DisposableStack
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,
it 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 DisposableStack();
const resource1 = stack.use(getResource1());
const resource2 = stack.use(getResource2());
const resource3 = stack.use(getResource3());
stack[Symbol.dispose](); // disposes 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 DisposableStack
and 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.AsyncDisposableStack
The ability to create a disposable resource from a callback has several benefits:
- It allows developers to leverage
using
while working with existing resources that do not conform to theSymbol.dispose
mechanic:{ using stack = new DisposableStack(); const reader = stack.adopt(createReader(), reader => reader.releaseLock()); ... }
- It grants user the ability to schedule other cleanup work to evaluate at the end of the block similar to Go's
defer
statement:function f() { using stack = new DisposableStack(); console.log("enter"); stack.defer(() => console.log("exit")); ... }
A user-defined disposable class might need to allocate and track multiple nested resources that should be disposed when
the class instance is disposed. However, properly managing the lifetime of these nested resources in the class
constructor can sometimes be difficult. The move
method of DisposableStack
/ helps to more
easily manage lifetime in these scenarios:AsyncDisposableStack
class PluginHost {
#disposed = false;
#disposables;
#channel;
#socket;
constructor() {
// Create a DisposableStack that is disposed when the constructor exits.
// If construction succeeds, we move everything out of `stack` and into
// `#disposables` to be disposed later.
using stack = new DisposableStack();
// Create an IPC adapter around process.send/process.on("message").
// When disposed, it unsubscribes from process.on("message").
this.#channel = stack.use(new NodeProcessIpcChannelAdapter(process));
// Create a pseudo-websocket that sends and receives messages over
// a NodeJS IPC channel.
this.#socket = stack.use(new NodePluginHostIpcSocket(this.#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`.
this.#disposables = stack.move();
// If construction failed, then `stack` would be disposed before reaching
// the line above. Event handlers would be removed, allowing `#channel` and
// `#socket` to be GC'd.
}
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.");
// ...
}
[Symbol.dispose]() {
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.dispose]()`, 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`
disposables[Symbol.dispose]();
}
}
}
You can also use a DisposableStack
to assist with disposal in a subclass constructor whose superclass is disposable:
class DerivedPluginHost extends PluginHost {
constructor() {
super();
// Create a DisposableStack to cover the subclass constructor.
using stack = new DisposableStack();
// Defer a callback to dispose resources on the superclass. We use `defer` so that we can invoke the version of
// `[Symbol.dispose]` on the superclass and not on this or any subclasses.
stack.defer(() => super[Symbol.dispose]());
// If any operations throw during subclass construction, the instance will still be disposed, and superclass
// resources will be freed
doSomethingThatCouldPotentiallyThrow();
// As the last step before exiting, empty out the DisposableStack so that we don't dispose ourselves.
stack.move();
}
}
Here, we can use stack
to track the result of super()
(i.e., the this
value). If any exception occurs during
subclass construction, we can ensure that [Symbol.dispose]()
is called, freeing resources. If the subclass also needs
to track its own disposable resources, this example is modified slightly:
class DerivedPluginHostWithOwnDisposables extends PluginHost {
#logger;
#disposables;
constructor() {
super()
// Create a DisposableStack to cover the subclass constructor.
using stack = new DisposableStack();
// Defer a callback to dispose resources on the superclass. We use `defer` so that we can invoke the version of
// `[Symbol.dispose]` on the superclass and not on this or any subclasses.
stack.defer(() => super[Symbol.dispose]());
// Create a logger that uses the file system and add it to our own disposables.
this.#logger = stack.use(new FileLogger());
// If any operations throw during subclass construction, the instance will still be disposed, and superclass
// resources will be freed
doSomethingThatCouldPotentiallyThrow();
// Persist our own disposables. If construction fails prior to the call to `stack.move()`, our own disposables
// will be disposed before they are set, and then the superclass `[Symbol.dispose]` will be invoked.
this.#disposables = stack.move();
}
[Symbol.dispose]() {
this.#logger = undefined;
// Dispose of our resources and those of our superclass. We do not need to invoke `super[Symbol.dispose]()` since
// that is already tracked by the `stack.defer` call in the constructor.
this.#disposables[Symbol.dispose]();
}
}
In this example, we can simply add new resources to the stack
and move its contents into the subclass instance's
this.#disposables
. In the subclass [Symbol.dispose]()
method we don't need to call super[Symbol.dispose]()
since
that has already been tracked by the stack.defer
call in the constructor.
Iterators in ECMAScript also employ a "cleanup" step by way of supplying a return
method. This means that there is
some similarity between a using
declaration and a for..of
statement:
// using
function f() {
using x = ...;
// use x
} // x is disposed
// for..of
function makeDisposableScope() {
const resources = [];
let state = 0;
return {
next() {
switch (state) {
case 0:
state++;
return {
done: false,
value: {
use(value) {
resources.unshift(value);
return value;
}
}
};
case 1:
state++;
for (const value of resources) {
value?.[Symbol.dispose]();
}
default:
state = -1;
return { done: true };
}
},
return() {
switch (state) {
case 1:
state++;
for (const value of resources) {
value?.[Symbol.dispose]();
}
default:
state = -1;
return { done: true };
}
},
[Symbol.iterator]() { return this; }
}
}
function f() {
for (const { use } of makeDisposableScope()) {
const x = use(...);
// use x
} // x is disposed
}
However there are a number drawbacks to using for..of
as an alternative:
- Exceptions in the body are swallowed by exceptions from disposables.
for..of
implies iteration, which can be confusing when reading code.- Conflating
for..of
and resource management could make it harder to find documentation, examples, StackOverflow answers, etc. - A
for..of
implementation like the one above cannot control the scope ofuse
, which can make lifetimes confusing:for (const { use } of ...) { const x = use(...); // ok setImmediate(() => { const y = use(...); // wrong lifetime }); }
- Significantly more boilerplate compared to
using
. - Mandates introduction of a new block scope, even at the top level of a function body.
- Control flow analysis of a
for..of
loop cannot infer definite assignment since a loop could potentially have zero elements:// using function f1() { /** @type {string | undefined} */ let x; { using y = ...; x = y.text; } x.toString(); // x is definitely assigned } // for..of function f2() { /** @type {string | undefined} */ let x; for (const { use } of ...) { const y = use(...); x = y.text; } x.toString(); // possibly an error in a static analyzer since `x` is not guaranteed to have been assigned. }
- Using
continue
andbreak
is more difficult if you need to dispose of an iterated value:// using for (using x of iterable) { if (!x.ready) continue; if (x.done) break; ... } // for..of outer: for (const x of iterable) { for (const { use } of ...) { use(x); if (!x.ready) continue outer; if (!x.done) break outer; ... } }
This proposal does not necessarily require immediate support in the HTML DOM specification, 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 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: A summary of DOM APIs relevant to async disposal can be found in the Async Explicit Resource Management proposal.
BroadcastChannel
—@@dispose()
as an alias or wrapper forclose()
.EventSource
—@@dispose()
as an alias or wrapper forclose()
.FileReader
—@@dispose()
as an alias or wrapper forabort()
.IDbTransaction
—@@dispose()
could invokeabort()
if the transaction is still in the active state:{ using tx = db.transaction(storeNames); // ... if (...) throw new Error(); // ... tx.commit(); } // implicit tx.abort() if we don't reach the explicit tx.commit()
ImageBitmap
—@@dispose()
as an alias or wrapper forclose()
.IntersectionObserver
—@@dispose()
as an alias or wrapper fordisconnect()
.MessagePort
—@@dispose()
as an alias or wrapper forclose()
.MutationObserver
—@@dispose()
as an alias or wrapper fordisconnect()
.PaymentRequest
—@@asyncDispose()
could invokeabort()
if the payment is still in the active state.- NOTE:
abort()
here is asynchronous, but uses the same name as similar synchronous methods on other objects.
- NOTE:
PerformanceObserver
—@@dispose()
as an alias or wrapper fordisconnect()
.RTCPeerConnection
—@@dispose()
as an alias or wrapper forclose()
.RTCRtpTransceiver
—@@dispose()
as an alias or wrapper forstop()
.ReadableStreamDefaultController
—@@dispose()
as an alias or wrapper forclose()
.ReadableStreamDefaultReader
— Either@@dispose()
as an alias or wrapper forreleaseLock()
, orResizeObserver
—@@dispose()
as an alias or wrapper fordisconnect()
.SourceBuffer
—@@dispose()
as a wrapper forabort()
.TransformStreamDefaultController
—@@dispose()
as an alias or wrapper forterminate()
.WebSocket
—@@dispose()
as a wrapper forclose()
.Worker
—@@dispose()
as an alias or wrapper forterminate()
.WritableStreamDefaultWriter
— Either@@dispose()
as an alias or wrapper forreleaseLock()
, orXMLHttpRequest
—@@dispose()
as an alias or wrapper forabort()
.
In addition, several new APIs could be considered that leverage this functionality:
EventTarget.prototype.addEventListener(type, listener, { subscription: true }) -> Disposable
— An option passed toaddEventListener
could return aDisposable
that removes the event listener when disposed.Performance.prototype.measureBlock(measureName, options) -> Disposable
— Combinesmark
andmeasure
into a block-scoped disposable:function f() { using measure = performance.measureBlock("f"); // marks on entry // ... } // marks and measures on exit
SVGSVGElement
— A new method producing a single-use disposer forpauseAnimations()
andunpauseAnimations()
.ScreenOrientation
— A new method producing a single-use disposer forlock()
andunlock()
.
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: A summary of NodeJS APIs relevant to async disposal can be found in the Async Explicit Resource Management proposal.
- Anything with
ref()
andunref()
methods — A new method or API that produces a single-use disposer forref()
andunref()
. - Anything with
cork()
anduncork()
methods — A new method or API that produces a single-use disposer forcork()
anduncork()
. async_hooks.AsyncHook
— either@@dispose()
as an alias or wrapper fordisable()
, or a new method that produces a single-use disposer forenable()
anddisable()
.child_process.ChildProcess
—@@dispose()
as an alias or wrapper forkill()
.cluster.Worker
—@@dispose()
as an alias or wrapper forkill()
.crypto.Cipher
,crypto.Decipher
—@@dispose()
as a wrapper forfinal()
.crypto.Hash
,crypto.Hmac
—@@dispose()
as a wrapper fordigest()
.dns.Resolver
,dnsPromises.Resolver
—@@dispose()
as an alias or wrapper forcancel()
.domain.Domain
— A new method or API that produces a single-use disposer forenter()
andexit()
.events.EventEmitter
— A new method or API that produces a single-use disposer foron()
andoff()
.fs.FSWatcher
—@@dispose()
as an alias or wrapper forclose()
.http.Agent
—@@dispose()
as an alias or wrapper fordestroy()
.http.ClientRequest
— Either@@dispose()
or@@asyncDispose()
(see Async Explicit Resource Management) as an alias or wrapper fordestroy()
.http.IncomingMessage
— Either@@dispose()
or@@asyncDispose()
(see Async Explicit Resource Management) as an alias or wrapper fordestroy()
.http.OutgoingMessage
— Either@@dispose()
or@@asyncDispose()
(see Async Explicit Resource Management) as an alias or wrapper fordestroy()
.http2.Http2ServerRequest
— Either@@dispose()
or@@asyncDispose()
(see Async Explicit Resource Management) as an alias or wrapper fordestroy()
.inspector
— A new API that produces a single-use disposer foropen()
andclose()
.stream.Writable
— Either@@dispose()
or@@asyncDispose()
(see Async Explicit Resource Management) as an alias or wrapper fordestroy()
or@@asyncDispose
only as a callback-adapting wrapper forend()
(depending on whether the disposal behavior should be to drop immediately or to flush any pending writes).stream.Readable
— Either@@dispose()
or@@asyncDispose()
(see Async Explicit Resource Management) as an alias or wrapper fordestroy()
.- ... and many others in
net
,readline
,tls
,udp
, andworker_threads
.
Several pieces of functionality related to this proposal are currently out of scope. However, we still feel they are important characteristics for the ECMAScript to employ in the future and may be considered for follow-on proposals:
- RAII-style
async using
declarations (i.e.,async using id = expr
) for async disposables — Postponed to Follow-on Proposal Symbol.asyncDispose
— Postponed to Follow-on ProposalAsyncDisposableStack
— Postponed to Follow-on Proposal- Bindingless
using void
declarations (i.e.,using void = expr
) — Postponed to Follow-on Proposal - Block-style
using
statements (i.e.,using (x = expr) {}
) for sync disposables — - Withdrawn. - Block-style
using await
statements (i.e.,using await (x = expr) {}
) for async disposables - Withdrawn.
- TC39 July 24th, 2018
- Conclusion
- Stage 1 acceptance
- Conclusion
- TC39 July 23rd, 2019
- Conclusion
- Table until Thursday, inconclusive.
- Conclusion
- TC39 July 25th, 2019
- Conclusion:
- Investigate Syntax
- Approved for Stage 2
- YK (@wycatz) & WH (@waldemarhorwat) will be stage 3 reviewers
- Conclusion:
- TC39 October 10th, 2021
- Conclusion
- Status Update only
- WH Continuing to review
- SYG (@syg) added as reviewer
- Conclusion
- TC39 December 1st, 2022 (notes TBA)
- Conclusion
using
declarations,Symbol.dispose
, andDisposableStack
advanced to Stage 3, under the following conditions:- Resolution of #103 - Argument order for
adopt()
- Deferral of
async using
declarations,Symbol.asyncDispose
, andAsyncDisposableStack
.
- Resolution of #103 - Argument order for
async using
declarations,Symbol.asyncDispose
, andAsyncDisposableStack
remain at Stage 2 as an independent proposal.
- Conclusion
The following is a high-level list of tasks to progress through each stage of the TC39 proposal process:
- Identified a "champion" who will advance the addition.
- Prose outlining the problem or need and the general shape of a solution.
- Illustrative examples of usage.
- High-level API.
- Initial specification text.
- Transpiler support (Optional).
- Complete specification text.
- Designated reviewers have signed off on the current spec text:
- The ECMAScript editor has signed off on the current spec text.
- Test262 acceptance tests have been written for mainline usage scenarios and merged.
- Two compatible implementations which pass the acceptance tests: [1], [2].
- A pull request has been sent to tc39/ecma262 with the integrated spec text.
- The ECMAScript editor has signed off on the pull request.