Skip to content

tc39/proposal-explicit-resource-management

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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