• Stars
    star
    746
  • Rank 60,823 (Top 2 %)
  • Language
    JavaScript
  • License
    BSD 3-Clause "New...
  • Created almost 7 years ago
  • Updated 3 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

ECMAScript Explicit Resource Management

ECMAScript Explicit Resource Management

NOTE: This proposal has subsumed the Async Explicit Resource Management proposal. This proposal repository should be used for further discussion of both sync and async of explicit resource management.

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 and 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:

// sync generators
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`
}
// async generators
async function * g() {
  const handle = 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:

// sync disposal
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`
// async disposal
async function * g() {
  using stream = acquireStream(); // block-scoped critical resource
  ...
} // cleanup

{
  await using obj = g(); // block-scoped declaration
  const r = await 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.

Status

Stage: 3
Champion: Ron Buckton (@rbuckton)
Last Presented: March, 2023 (slides, notes #1, notes #2)

For more information see the TC39 proposal process.

Authors

  • Ron Buckton (@rbuckton)

Motivations

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)
  • 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:

    // sync disposal
    { // 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 = ...;
    ...
    // async sync disposal
    { // 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 = ...;
    ...
  • 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'
    }

Prior Art

Definitions

  • Resource — An object with a specific lifetime, at the end of which either a lifetime-sensitive operation should be performed or a non-garbage-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 keys
    • WeakSet values
    • WeakRef values
    • FinalizationRegistry 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 like using).

Syntax

using Declarations

// a synchronously-disposed, block-scoped resource
using x = expr1;            // resource w/ local binding
using y = expr2, z = expr4; // multiple resources

Grammar

Please refer to the specification text for the most recent version of the grammar.

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:

  • The top level of a Module anywhere VariableStatement is allowed, as long as it is not immediately nested inside of a CaseClause or DefaultClause.
  • In the body of an async function or async generator anywhere a VariableStatement is allowed, as long as it is not immediately nested inside of a CaseClause or DefaultClause.
  • In the head of a for-of or for-await-of statement.

await using in for-of and for-await-of Statements

for (await using x of y) ...

for await (await using x of y) ...

You can use an 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

Semantics

using Declarations

using Declarations with Explicit Local Bindings

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.

using Declarations with Multiple Resources

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.

using Declarations and null or undefined Values

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

using Declarations and Values Without [Symbol.dispose]

If a resource does not have a callable [Symbol.dispose] member, a TypeError would be thrown immediately when the resource is tracked.

using Declarations in for-of and for-await-of Loops

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.

await using Declarations

await using Declarations with Explicit Local Bindings

UsingDeclaration :
  `await` `using` 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 Resources

An 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 Values

This proposal has opted to ignore null and undefined values provided to await using declarations. This is consistent with the proposed behavior for the using declarations in this proposal. Like in the sync case, this allows simplifying 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 Loops

An 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.

Implicit Async Interleaving Points ("implicit 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 an 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 an 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 an await using statement in well-formatted 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.

Examples

The following show examples of using this proposal with various APIs, assuming those APIs adopted this proposal.

WHATWG Streams API

{
  using reader = stream.getReader();
  const { value, done } = reader.read();
} // 'reader' is disposed

NodeJS FileHandle

{
  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

NodeJS Streams

{
  await using writable = ...;
  writable.write(...);
} // 'writable.end()' is called and its result is awaited

Logging and tracing

// audit privileged function call entry and exit
function privilegedActivity() {
  using activity = auditLog.startActivity("privilegedActivity"); // log activity start
  ...
} // log activity end

Async Coordination

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

Three-Phase Commit Transactions

// 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

Shared Structs

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

API

Additions to Symbol

This proposal adds the dispose and asyncDispose properties to the Symbol constructor, whose values are the @@dispose and @@asyncDispose internal symbols:

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.
@@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;
  readonly dispose: unique symbol;
}

The SuppressedError Error

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, while SuppressedError 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 through cause, however a SuppressedError isn't "caused" by the error it suppresses. In addition, cause is intended to be optional, while the error of a SuppressedError must always be defined.

Built-in Disposables

%IteratorPrototype%.@@dispose()

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();
}

%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();
}

Other Possibilities

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.

The Common Disposable and AsyncDisposable Interfaces

The Disposable Interface

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;
}

The AsyncDisposable Interface

An 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>;
}

The DisposableStack and AsyncDisposableStack container objects

This proposal adds two global objects that can act as containers 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];
}

AsyncDisposableStack is the async version of DisposableStack 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):

These classes provided the following capabilities:

  • Aggregation
  • Interoperation and customization
  • Assist in complex construction

NOTE: DisposableStack is inspired by Python's ExitStack.

NOTE: AsyncDisposableStack is inspired by Python's AsyncExitStack.

Aggregation

The DisposableStack and AsyncDisposableStack classes provid the ability to aggregate multiple disposable resources into a single container. When the 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:

// sync
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
// async
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
  )
)

Interoperation and Customization

The DisposableStack and AsyncDisposableStack classes also provid 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:

  • It allows developers to leverage using/await using while working with existing resources that do not conform to the Symbol.dispose/Symbol.asyncDispose 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"));
      ...
    }

Assist in Complex Construction

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/AsyncDisposableStack helps to more easily manage lifetime in these scenarios:

// sync
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]();
    }
  }
}
// async
const privateConstructorSentinel = {};

class AsyncPluginHost {
  #disposed = false;
  #disposables;
  #channel;
  #socket;

  /** @private */
  constructor(arg) {
    if (arg !== privateConstructorSentinel) throw new TypeError("Use AsyncPluginHost.create() instead");
  }
  
  // NOTE: there's no such thing as an async constructor
  static async create() {
    const host = new AsyncPluginHost(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]();
    }
  }
}

Subclassing Disposable Classes

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.

Relation to Iterator and for..of

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 of use, 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 and break 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;
        ...
      }
    }

Relation to DOM APIs

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.

  • AudioContext — @@asyncDispose() as an alias or wrapper for close().
    • NOTE: close() here is asynchronous, but uses the same name as similar synchronous methods on other objects.
  • BroadcastChannel — @@dispose() as an alias or wrapper for close().
  • EventSource — @@dispose() as an alias or wrapper for close().
  • FileReader — @@dispose() as an alias or wrapper for abort().
  • IDbTransaction — @@dispose() could invoke abort() 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 for close().
  • IntersectionObserver — @@dispose() as an alias or wrapper for disconnect().
  • MediaKeySession — @@asyncDispose() as an alias or wrapper for close().
    • NOTE: close() here is asynchronous, but uses the same name as similar synchronous methods on other objects.
  • MessagePort — @@dispose() as an alias or wrapper for close().
  • MutationObserver — @@dispose() as an alias or wrapper for disconnect().
  • PaymentRequest — @@asyncDispose() could invoke abort() 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.
  • PerformanceObserver — @@dispose() as an alias or wrapper for disconnect().
  • 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).
  • RTCPeerConnection — @@dispose() as an alias or wrapper for close().
  • RTCRtpTransceiver — @@dispose() as an alias or wrapper for stop().
  • ReadableStreamDefaultController — @@dispose() as an alias or wrapper for close().
  • ReadableStreamDefaultReader — Either @@dispose() as an alias or wrapper for releaseLock(), or
  • ResizeObserver — @@dispose() as an alias or wrapper for disconnect().
  • ServiceWorkerRegistration — @@asyncDispose() as a wrapper for unregister().
  • SourceBuffer — @@dispose() as a wrapper for abort().
  • TransformStreamDefaultController — @@dispose() as an alias or wrapper for terminate().
  • WebSocket — @@dispose() as a wrapper for close().
  • Worker — @@dispose() as an alias or wrapper for terminate().
  • WritableStream — @@asyncDispose() as an alias or wrapper for close().
    • NOTE: 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).
  • XMLHttpRequest — @@dispose() as an alias or wrapper for abort().

In addition, several new APIs could be considered that leverage this functionality:

  • EventTarget.prototype.addEventListener(type, listener, { subscription: true }) -> Disposable — An option passed to addEventListener could return a Disposable that removes the event listener when disposed.
  • Performance.prototype.measureBlock(measureName, options) -> Disposable — Combines mark and measure 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 for pauseAnimations() and unpauseAnimations().
  • ScreenOrientation — A new method producing a single-use disposer for lock() and unlock().

Definitions

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).

Relation to NodeJS APIs

This proposal does not necessarily require immediate support in NodeJS, 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 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.

  • Anything with ref() and unref() methods — A new method or API that produces a single-use disposer for ref() and unref().
  • Anything with cork() and uncork() methods — A new method or API that produces a single-use disposer for cork() and uncork().
  • async_hooks.AsyncHook — either @@dispose() as an alias or wrapper for disable(), or a new method that produces a single-use disposer for enable() and disable().
  • child_process.ChildProcess — @@dispose() as an alias or wrapper for kill().
  • cluster.Worker — @@dispose() as an alias or wrapper for kill().
  • crypto.Cipher, crypto.Decipher — @@dispose() as a wrapper for final().
  • crypto.Hash, crypto.Hmac — @@dispose() as a wrapper for digest().
  • dns.Resolver, dnsPromises.Resolver — @@dispose() as an alias or wrapper for cancel().
  • domain.Domain — A new method or API that produces a single-use disposer for enter() and exit().
  • events.EventEmitter — A new method or API that produces a single-use disposer for on() and off().
  • 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().
  • fs.FSWatcher — @@dispose() as an alias or wrapper for close().
  • http.Agent — @@dispose() as an alias or wrapper for destroy().
  • 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().
  • inspector — A new API that produces a single-use disposer for open() and 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().
  • ... and many others in net, readline, tls, udp, and worker_threads.

Meeting Notes

TODO

The following is a high-level list of tasks to progress through each stage of the TC39 proposal process:

Stage 1 Entrance Criteria

  • 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.

Stage 2 Entrance Criteria

Stage 3 Entrance Criteria

Stage 4 Entrance Criteria

  • 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.

Implementations

  • Built-ins from this proposal are available in core-js

More Repositories

1

proposals

Tracking ECMAScript Proposals
17,177
star
2

ecma262

Status, process, and documents for ECMA-262
HTML
14,437
star
3

proposal-pipeline-operator

A proposal for adding a useful pipe operator to JavaScript.
HTML
7,534
star
4

proposal-pattern-matching

Pattern matching syntax for ECMAScript
HTML
5,498
star
5

proposal-optional-chaining

HTML
4,942
star
6

proposal-type-annotations

ECMAScript proposal for type syntax that is erased - Stage 1
JavaScript
4,252
star
7

proposal-signals

A proposal to add signals to JavaScript.
3,387
star
8

proposal-temporal

Provides standard objects and functions for working with dates and times.
HTML
3,321
star
9

proposal-observable

Observables for ECMAScript
JavaScript
3,058
star
10

proposal-decorators

Decorators for ES6 classes
2,640
star
11

proposal-record-tuple

ECMAScript proposal for the Record and Tuple value types. | Stage 2: it will change!
HTML
2,496
star
12

test262

Official ECMAScript Conformance Test Suite
JavaScript
2,073
star
13

proposal-dynamic-import

import() proposal for JavaScript
HTML
1,863
star
14

proposal-bind-operator

This-Binding Syntax for ECMAScript
1,742
star
15

proposal-class-fields

Orthogonally-informed combination of public and private fields proposals
HTML
1,722
star
16

proposal-async-await

Async/await for ECMAScript
HTML
1,578
star
17

proposal-object-rest-spread

Rest/Spread Properties for ECMAScript
HTML
1,493
star
18

proposal-shadowrealm

ECMAScript Proposal, specs, and reference implementation for Realms
HTML
1,429
star
19

proposal-iterator-helpers

Methods for working with iterators in ECMAScript
HTML
1,307
star
20

proposal-nullish-coalescing

Nullish coalescing proposal x ?? y
HTML
1,232
star
21

proposal-top-level-await

top-level `await` proposal for ECMAScript (stage 4)
HTML
1,083
star
22

proposal-partial-application

Proposal to add partial application to ECMAScript
HTML
1,002
star
23

proposal-do-expressions

Proposal for `do` expressions
HTML
990
star
24

proposal-binary-ast

Binary AST proposal for ECMAScript
961
star
25

agendas

TC39 meeting agendas
JavaScript
952
star
26

proposal-built-in-modules

HTML
891
star
27

proposal-async-iteration

Asynchronous iteration for JavaScript
HTML
857
star
28

proposal-set-methods

Proposal for new Set methods in JS
HTML
655
star
29

proposal-string-dedent

TC39 Proposal to remove common leading indentation from multiline template strings
HTML
614
star
30

proposal-operator-overloading

JavaScript
610
star
31

proposal-import-attributes

Proposal for syntax to import ES modules with assertions
HTML
591
star
32

proposal-async-context

Async Context for JavaScript
HTML
587
star
33

proposal-bigint

Arbitrary precision integers in JavaScript
HTML
561
star
34

ecmascript_simd

SIMD numeric type for EcmaScript
JavaScript
540
star
35

ecma402

Status, process, and documents for ECMA 402
HTML
529
star
36

proposal-slice-notation

HTML
523
star
37

proposal-change-array-by-copy

Provides additional methods on Array.prototype and TypedArray.prototype to enable changes on the array by returning a new copy of it with the change.
HTML
511
star
38

notes

TC39 meeting notes
JavaScript
496
star
39

proposal-class-public-fields

Stage 2 proposal for public class fields in ECMAScript
HTML
489
star
40

proposal-iterator.range

A proposal for ECMAScript to add a built-in Iterator.range()
HTML
483
star
41

proposal-decimal

Built-in exact decimal numbers for JavaScript
HTML
477
star
42

proposal-uuid

UUID proposal for ECMAScript (Stage 1)
JavaScript
463
star
43

proposal-module-expressions

HTML
433
star
44

proposal-throw-expressions

Proposal for ECMAScript 'throw' expressions
JavaScript
425
star
45

proposal-UnambiguousJavaScriptGrammar

413
star
46

proposal-weakrefs

WeakRefs
HTML
409
star
47

proposal-array-grouping

A proposal to make grouping of array items easier
HTML
407
star
48

proposal-error-cause

TC39 proposal for accumulating errors
HTML
380
star
49

proposal-cancelable-promises

Former home of the now-withdrawn cancelable promises proposal for JavaScript
Shell
376
star
50

proposal-ecmascript-sharedmem

Shared memory and atomics for ECMAscript
HTML
374
star
51

proposal-module-declarations

JavaScript Module Declarations
HTML
369
star
52

proposal-first-class-protocols

a proposal to bring protocol-based interfaces to ECMAScript users
352
star
53

proposal-relative-indexing-method

A TC39 proposal to add an .at() method to all the basic indexable classes (Array, String, TypedArray)
HTML
351
star
54

proposal-global

ECMAScript Proposal, specs, and reference implementation for `global`
HTML
346
star
55

proposal-private-methods

Private methods and getter/setters for ES6 classes
HTML
345
star
56

proposal-numeric-separator

A proposal to add numeric literal separators in JavaScript.
HTML
330
star
57

proposal-private-fields

A Private Fields Proposal for ECMAScript
HTML
319
star
58

tc39.github.io

Get involved in specifying JavaScript
HTML
318
star
59

proposal-object-from-entries

TC39 proposal for Object.fromEntries
HTML
318
star
60

proposal-promise-allSettled

ECMAScript Proposal, specs, and reference implementation for Promise.allSettled
HTML
314
star
61

proposal-await.ops

Introduce await.all / await.race / await.allSettled / await.any to simplify the usage of Promises
HTML
310
star
62

proposal-regex-escaping

Proposal for investigating RegExp escaping for the ECMAScript standard
JavaScript
309
star
63

proposal-export-default-from

Proposal to add `export v from "mod";` to ECMAScript.
HTML
306
star
64

proposal-logical-assignment

A proposal to combine Logical Operators and Assignment Expressions
HTML
302
star
65

proposal-promise-finally

ECMAScript Proposal, specs, and reference implementation for Promise.prototype.finally
HTML
279
star
66

proposal-json-modules

Proposal to import JSON files as modules
HTML
272
star
67

proposal-asset-references

Proposal to ECMAScript to add first-class location references relative to a module
270
star
68

proposal-cancellation

Proposal for a Cancellation API for ECMAScript
HTML
267
star
69

proposal-promise-with-resolvers

HTML
255
star
70

proposal-string-replaceall

ECMAScript proposal: String.prototype.replaceAll
HTML
253
star
71

proposal-export-ns-from

Proposal to add `export * as ns from "mod";` to ECMAScript.
HTML
242
star
72

proposal-structs

JavaScript Structs: Fixed Layout Objects
230
star
73

proposal-ses

Draft proposal for SES (Secure EcmaScript)
HTML
223
star
74

proposal-intl-relative-time

`Intl.RelativeTimeFormat` specification [draft]
HTML
215
star
75

proposal-json-parse-with-source

Proposal for extending JSON.parse to expose input source text.
HTML
214
star
76

proposal-flatMap

proposal for flatten and flatMap on arrays
HTML
214
star
77

proposal-defer-import-eval

A proposal for introducing a way to defer evaluate of a module
HTML
208
star
78

ecmarkup

An HTML superset/Markdown subset source format for ECMAScript and related specifications
TypeScript
201
star
79

proposal-promise-any

ECMAScript proposal: Promise.any
HTML
200
star
80

proposal-optional-chaining-assignment

`a?.b = c` proposal
186
star
81

proposal-decorators-previous

Decorators for ECMAScript
HTML
184
star
82

proposal-smart-pipelines

Old archived draft proposal for smart pipelines. Go to the new Hack-pipes proposal at js-choi/proposal-hack-pipes.
HTML
181
star
83

proposal-array-from-async

Draft specification for a proposed Array.fromAsync method in JavaScript.
HTML
178
star
84

proposal-upsert

ECMAScript Proposal, specs, and reference implementation for Map.prototype.upsert
HTML
176
star
85

proposal-collection-methods

HTML
171
star
86

proposal-array-filtering

A proposal to make filtering arrays easier
HTML
171
star
87

proposal-ptc-syntax

Discussion and specification for an explicit syntactic opt-in for Tail Calls.
HTML
169
star
88

proposal-extractors

Extractors for ECMAScript
JavaScript
166
star
89

proposal-error-stacks

ECMAScript Proposal, specs, and reference implementation for Error.prototype.stack / System.getStack
HTML
166
star
90

proposal-intl-duration-format

164
star
91

how-we-work

Documentation of how TC39 operates and how to participate
161
star
92

proposal-Array.prototype.includes

Spec, tests, reference implementation, and docs for ESnext-track Array.prototype.includes
HTML
157
star
93

proposal-promise-try

ECMAScript Proposal, specs, and reference implementation for Promise.try
HTML
154
star
94

proposal-extensions

Extensions proposal for ECMAScript
HTML
150
star
95

proposal-hashbang

#! for JS
HTML
148
star
96

proposal-import-meta

import.meta proposal for JavaScript
HTML
146
star
97

proposal-intl-segmenter

Unicode text segmentation for ECMAScript
HTML
146
star
98

proposal-resizablearraybuffer

Proposal for resizable array buffers
HTML
145
star
99

proposal-seeded-random

Proposal for an options argument to be added to JS's Math.random() function, and some options to start it with.
HTML
143
star
100

eshost

A uniform wrapper around a multitude of ECMAScript hosts. CLI: https://github.com/bterlson/eshost-cli
JavaScript
142
star