All Projects → tc39 → Proposal Explicit Resource Management

tc39 / Proposal Explicit Resource Management

Licence: bsd-3-clause
ECMAScript Explicit Resource Management

Labels

ECMAScript 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 expose this pattern through the return method, as a means to explicitly evaluate finally blocks to ensure user-defined cleanup logic is preserved:

function * g() {
  const handle = acquireFileHandle(); // critical resource
  try {
    ...
  }
  finally {
    handle.release(); // cleanup
  }
}

const obj = g();
try {
  const r = obj.next();
  ...
}
finally {
  obj.return(); // calls finally blocks in `g`
}

As such, we propose the adoption of a syntax to simplify this common pattern:

try using Blocks

function * g() {
  try using (const handle = acquireFileHandle()) { // critical resource
    ...
  } // cleanup
  
  // or, if `handle` binding is unused:
  try using (acquireFileHandle()) { // critical resource
    ...
  } // cleanup
}

try using (const obj = g()) {
  const r = obj.next();
  ...
} // calls finally blocks in `g`

using const Declarations and using value Statements

function * g() {
  using const handle = acquireFileHandle(); // block-scoped critical resource

  // or, if `handle` binding is unused:
  using value acquireFileHandle(); // block-scoped critical resource
} // cleanup

{
  using const obj = g(); // block-scoped declaration
  const r = obj.next();
} // calls finally blocks in `g`

Status

Stage: 2
Champion: Ron Buckton (@rbuckton)
Last Presented: February, 2020 (slides, notes)

For more information see the TC39 proposal process.

Authors

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:
    { // 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
    try using (const a = ..., b = ...) { 
      ...
    }
    
  • Non memory/IO applications:
    import { ReaderWriterLock } from "prex";
    const lock = new ReaderWriterLock(); 
    
    export async function readData() {
      // wait for outstanding writer and take a read lock
      try using (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
      try using (await lock.write()) { 
        ... // only one writer
        await ...;
        ... // still in write lock after `await`
      } // release the write lock
    }
    

Prior Art

Syntax

try using Blocks

// for a synchronously-disposed resource:

// 'try' with expression resource
try using (expr) {
  ...
}

// 'try' with local binding
try using (const x = expr1) {
  ...
}

// 'try' with multiple local bindings
try using (const x = expr1, y = expr2) {
  ...
}

// for an asynchronously disposed resource in an async function:

// 'try' with expression resource
try using await (obj) {
  ...
}

// 'try' with local binding
try using await (const x = expr1) {
  ...
}

// 'try' with multiple local bindings
try using await (const x = expr1, y = expr2) {
  ...
}

using value Statements and using const Declarations

// for a synchronously-disposed resource (block scoped):
using value expr;                         // no local binding
using const x = expr1;                    // local binding
using const y = expr2, z = expr3;         // multiple bindings

// for an asynchronously-disposed resource (block scoped):
using await value expr;                   // no local binding
using await const x = expr1;              // local binding
using await const y = expr2, z = expr3;   // multiple bindings

Grammar

Statement[Yield, Await, Return] :
  ...
  UsingValueStatement[?Yield, ?Await]

TryUsingDeclaration[Yield, Await] :
    `const` BindingList[+In, ?Yield, ?Await, +Using]

TryStatement[Yield, Await, Return] :
    ...
    `try` `using` `(` [lookahead ≠ `let [`] Expression[+In, ?Yield, ?Await] `)` Block[?Yield, ?Await, ?Return] Catch[?Yield, ?Await, ?Return]? Finally[?Yield, ?Await, ?Return]?
    `try` `using` `(` TryUsingDeclaration[?Yield, ?Await] `)` Block[?Yield, ?Await, ?Return] Catch[?Yield, ?Await, ?Return]? Finally[?Yield, ?Await, ?Return]?
    [+Await] `try` `using` `await` `(` [lookahead ≠ `let [`] Expression[+In, ?Yield, ?Await] `)` Block[?Yield, ?Await, ?Return] Catch[?Yield, ?Await, ?Return]? Finally[?Yield, ?Await, ?Return]?
    [+Await] `try` `using` `await` `(` TryUsingDeclaration[?Yield, ?Await] `)` Block[?Yield, ?Await, ?Return] Catch[?Yield, ?Await, ?Return]? Finally[?Yield, ?Await, ?Return]?

UsingValueStatement[Yield, Await] :
  `using` [no LineTerminator here] `value` AssignmentExpression[+In, ?Yield, ?Await] `;`
  [+Await] `using` [no LineTerminator here] `await` [no LineTerminator here] `value` AssignmentExpression[+In, ?Yield, +Await] `;`

LexicalDeclaration[In, Yield, Await] :
  LetOrConst BindingList[?In, ?Yield, ?Await, ~Using] `;`
  `using` [no LineTerminator here] `const` BindingList[?In, ?Yield, ?Await, +Using] `;`
  [+Await] `using` [no LineTerminator here] `await` [no LineTerminator here] `const` BindingList[?In, ?Yield, +Await, +Using] `;`

BindingList[In, Yield, Await, Using] :
  LexicalBinding[?In, ?Yield, ?Await, ?Using]
  BindingList[?In, ?Yield, ?Await, ?Using] `,` LexicalBinding[?In, ?Yield, ?Await, ?Using]

LexicalBinding[In, Yield, Await, Using] :
  BindingIdentifier[?Yield, ?Await] Initializer[?In, ?Yield, ?Await]?
  [~Using] BindingPattern[?Yield, ?Await] Initializer[?In, ?Yield, ?Await]

Semantics

try using Blocks

try using with Existing Resources

TryStatement : 
    `try` `using` `(` Expression `)` Block Catch? Finally?
    `try` `using` `await` `(` Expression `)` Block Catch? Finally?

When try using is parsed with an Expression, an implicit block-scoped binding is created for the result of the expression. When the try using block is exited, whether by an abrupt or normal completion, [Symbol.dispose]() is called on the local binding as long as it is neither null nor undefined. If an error is thrown in both Block and the call to [Symbol.dispose](), an AggregateError containing both errors will be thrown instead.

try using (expr) {
  ...
}

The above example has similar runtime semantics as the following transposed representation:

{ 
  const $$try = { stack: [], errors: [] };
  try {
    const $$expr = expr;
    if ($$expr !== null && $$expr !== undefined) {
      const $$dispose = $$expr[Symbol.dispose];
      if (typeof $$dispose !== "function") throw new TypeError();
      $$try.stack.push({ value: $$expr, dispose: $$dispose });
    }

    ...

  }
  catch ($$error) {
    $$try.errors.push($$error);
  }
  finally {
    while ($$try.stack.length) {
      const { value: $$expr, dispose: $$dispose } = $$try.stack.pop();
      try {
        $$dispose.call($$expr);
      }
      catch ($$error) {
        $$try.errors.push($$error);
      }
    }
    if ($$try.errors.length > 1) {
      throw new AggregateError($$try.errors);
    }
    if ($$try.errors.length === 1) {
      throw $$try.errors[0];
    }
  }
}

The local block-scoped binding ensures that if expr above is reassigned, we still correctly close the resource we are explicitly tracking.

try using with Explicit Local Bindings

TryUsingDeclaration :
    `const` BindingList

TryStatement : 
    `try` `using` `(` TryUsingDeclaration `)` Block Catch? Finally?

When try using is parsed with a TryUsingDeclaration we track the bindings created in the declaration for disposal:

try using (const x = expr1, y = expr2) {
  ...
}

These implicit bindings are again used to perform resource disposal when the Block exits, however in this case [Symbol.dispose]() is called on the implicit bindings in the reverse order of their declaration. This is approximately equivalent to the following:

try using (const x = expr1) {
  try using (const y = expr2) {
    ...
  }
}

Both of the above cases would have similar runtime semantics as the following transposed representation:

{
  const $$try = { stack: [], errors: [] };
  try {
    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 });
    }

    ...

  }
  catch ($$error) {
    $$try.errors.push($$error);
  }
  finally {
    while ($$try.stack.length) {
      const { value: $$expr, dispose: $$dispose } = $$try.stack.pop();
      try {
        $$dispose.call($$expr);
      }
      catch ($$error) {
        $$try.errors.push($$error);
      }
    }
    if ($$try.errors.length > 1) {
      throw new AggregateError($$try.errors);
    }
    if ($$try.errors.length === 1) {
      throw $$try.errors[0];
    }
  }
}

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.

try using with Destructuring

The try using statement does not allow destructuring for local bindings. This is due to following reasons:

  • It is unclear as to what should be disposed (the object being destructured or the individual bindings).
  • If individual bindings are to be disposed, what happens to unused bindings (i.e., try using (const { x } = { x, y }) ...).

Instead, you should explicitly destructure before or after declaring your disposable resource:

// before (when each property is disposable)
const { x, y } = f();
try using (Disposable.from([x, y])) {
  ...
}

// avoid destructuring (when each property is disposable)
const obj = f();
try using (const x = obj.x, y = obj.y) {
  ...
}

// after (when the destructured object is disposable)
try using (const obj = f()) {
  const { x, y } = obj;
}

try using on null or undefined Values

This proposal has opted to ignore null and undefined values provided to the try using statement. This is similar to the behavior of using in C# that 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()) {
  try using (const resource = getResource()) {
    ... // (1) above
    resource.doSomething()
    ... // (2) above
  }
}
else {
  // duplicate code path above
  ... // (1) above
  ... // (2) above
}

Compared to:

try using (const resource = isResourceAvailable() ? getResource() : undefined) {
  ... // (1) do some work with or without resource
  resource?.doSomething();
  ... // (2) do some other work with or without resource
}

try using on 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.

try using with Catch or Finally

When resources are added to a try using block, a Catch or Finally clause may follow. In these cases, the Catch and Finally clauses are triggered after [Symbol.dispose]() is called. This is consistent with the fact that block-scoped bindings for resources would be unreachable outside of try using's Block:

try using (getResource()) { // or `try using (const x = getResource())`
  ...
}
catch {
  // resource has already been disposed
}
finally {
  // resource has already been disposed
}

The above example has the similar runtime semantics as the following transposed representation:

try {
  const $$try = { stack: [], errors: [] };
  try {
    const $$expr = getResource();
    if ($$expr !== null && $$expr !== undefined) {
      const $$dispose = $$expr[Symbol.dispose];
      if (typeof $$dispose !== "function") throw new TypeError();
      $$try.stack.push({ value: $$expr, dispose: $$dispose });
    }

    ...

  }
  catch ($$error) {
    $$try.errors.push($$error);
  }
  finally {
    while ($$try.stack.length) {
      const { value: $$expr, dispose: $$dispose } = $$try.stack.pop();
      try {
        $$dispose.call($$expr);
      }
      catch ($$error) {
        $$try.errors.push($$error);
      }
    }
    if ($$try.errors.length > 1) {
      throw new AggregateError($$try.errors);
    }
    if ($$try.errors.length === 1) {
      throw $$try.errors[0];
    }
  }
}
catch {
  // resource has already been disposed
}
finally {
  // resource has already been disposed
}

try using await in AsyncFunction, AsyncGeneratorFunction, or Module

In an AsyncFunction or an AsyncGeneratorFunction, or the top-level of a Module, when we evaluate a try using await block we first look for a [Symbol.asyncDispose] method before looking for a [Symbol.dispose] method. At the end of the block or module, if the method returns a value other than undefined, we Await the value before exiting:

try using await (const x = expr) {
  ...
}

Is semantically equivalent to the following transposed representation:

{
  const $$try = { stack: [], errors: [] };
  try {
    const $$expr = getResource();
    if ($$expr !== null && $$expr !== undefined) {
      let $$dispose = $$expr[Symbol.asyncDispose];
      if ($$dispose === undefined) {
        $$dispose = $$expr[Symbol.dispose];
      }
      if (typeof $$dispose !== "function") throw new TypeError();
      $$try.stack.push({ value: $$expr, dispose: $$dispose });
    }

    ...

  }
  catch ($$error) {
    $$try.errors.push($$error);
  }
  finally {
    while ($$try.stack.length) {
      const { value: $$expr, dispose: $$dispose } = $$try.stack.pop();
      try {
        const $$result = $$dispose.call($$expr);
        if ($$result !== undefined) {
          await $$result;
        }
      }
      catch ($$error) {
        $$try.errors.push($$error);
      }
    }
    if ($$try.errors.length > 1) {
      throw new AggregateError($$try.errors);
    }
    if ($$try.errors.length === 1) {
      throw $$try.errors[0];
    }
  }
}

using value Statements and using const Declarations

using value with Existing Resources

UsingValueStatement : 
    `using` `value` Expression `;`
    `using` `await` `value` Expression `;`

When using value is parsed with an Expression, an implicit block-scoped binding is created for the result of the expression. When the Block (or Script/Module at the top level) containing the using value statement is exited, whether by an abrupt or normal completion, [Symbol.dispose]() is called on the local binding as long as it is neither null nor undefined. If an error is thrown in both the containing Block/Script/Module and the call to [Symbol.dispose](), an AggregateError containing both errors will be thrown instead.

{
  ...
  using value expr; // in Block scope
  ...
}

The above example has similar runtime semantics as the following transposed representation:

{ 
  const $$try = { stack: [], errors: [] };
  try {
    ...

    const $$expr = expr; // evaluate `expr`
    if ($$expr !== null && $$expr !== undefined) {
      const $$dispose = $$expr[Symbol.dispose];
      if (typeof $$dispose !== "function") throw new TypeError();
      $$try.stack.push({ value: $$expr, dispose: $$dispose });
    }

    ...
  }
  catch ($$error) {
    $$try.errors.push($$error);
  }
  finally {
    while ($$try.stack.length) {
      const { value: $$expr, dispose: $$dispose } = $$try.stack.pop();
      try {
        $$dispose.call($$expr);
      }
      catch ($$error) {
        $$try.errors.push($$error);
      }
    }
    if ($$try.errors.length > 1) {
      throw new AggregateError($$try.errors);
    }
    if ($$try.errors.length === 1) {
      throw $$try.errors[0];
    }
  }
}

The local block-scoped binding ensures that if expr above is reassigned, we still correctly close the resource we are explicitly tracking.

using const with Explicit Local Bindings

LexicalDeclaration :
  `using` `const` BindingList `;`

When using const is parsed we track the bindings created in the declaration for disposal at the end of the containing Block, Script, or Module:

{
  ...
  using const x = expr1, y = expr2;
  ...
}

These implicit bindings are again used to perform resource disposal when the Block, Script, or Module exits, however in this case [Symbol.dispose]() is called on the implicit bindings in the reverse order of their declaration. This is approximately equivalent to the following:

{
  using const x = expr1;
  {
    using const y = expr2;
    ...
  }
}

Both of the above cases would have similar runtime semantics as the following transposed representation:

{
  const $$try = { stack: [], errors: [] };
  try {
    ...

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

    ...
  }
  catch ($$error) {
    $$try.errors.push($$error);
  }
  finally {
    while ($$try.stack.length) {
      const { value: $$expr, dispose: $$dispose } = $$try.stack.pop();
      try {
        $$dispose.call($$expr);
      }
      catch ($$error) {
        $$try.errors.push($$error);
      }
    }
    if ($$try.errors.length > 1) {
      throw new AggregateError($$try.errors);
    }
    if ($$try.errors.length === 1) {
      throw $$try.errors[0];
    }
  }
}

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 value and using const on null or undefined Values

This proposal has opted to ignore null and undefined values provided to the using value statement and using const declaration. 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 const resource = getResource();
  ... // (1) above
  resource.doSomething()
  ... // (2) above
}
else {
  // duplicate code path above
  ... // (1) above
  ... // (2) above
}

Compared to:

using const resource = isResourceAvailable() ? getResource() : undefined;
... // (1) do some work with or without resource
resource?.doSomething();
... // (2) do some other work with or without resource

using value and using const on Values Without [Symbol.dispose]

If a resource does not have a callable [Symbol.dispose] member (or [Symbol.asyncDispose] in the case of a using await ...), a TypeError would be thrown immediately when the resource is tracked.

using await value and using await const in AsyncFunction, AsyncGeneratorFunction, or Module

In an AsyncFunction or an AsyncGeneratorFunction, or the top-level of a Module, when we evaluate a using await value statement or using await const declaration we first look for a [Symbol.asyncDispose] method before looking for a [Symbol.dispose] method. At the end of the containing block or Module, if the method returns a value other than undefined, we Await the value before exiting:

{
  ...
  using await const x = expr;
  ...
}

Is semantically equivalent to the following transposed representation:

{
  const $$try = { stack: [], errors: [] };
  try {
    ...

    const x = expr;
    if (x !== null && x !== undefined) {
      let $$dispose = x[Symbol.asyncDispose];
      if ($$dispose === undefined) {
        $$dispose = x[Symbol.dispose];
      }
      if (typeof $$dispose !== "function") throw new TypeError();
      $$try.stack.push({ value: x, dispose: $$dispose });
    }

    ...
  }
  catch ($$error) {
    $$try.errors.push($$error);
  }
  finally {
    while ($$try.stack.length) {
      const { value: $$expr, dispose: $$dispose } = $$try.stack.pop();
      try {
        const $$result = $$dispose.call($$expr);
        if ($$result !== undefined) {
          await $$result;
        }
      }
      catch ($$error) {
        $$try.errors.push($$error);
      }
    }
    if ($$try.errors.length > 1) {
      throw new AggregateError($$try.errors);
    }
    if ($$try.errors.length === 1) {
      throw $$try.errors[0];
    }
  }
}

Built-in Disposables

%IteratorPrototype%[email protected]@dispose()

We also propose to add @@dispose to the built-in %IteratorPrototype% as if it had the following behavior:

%IteratorPrototype%[Symbol.dispose] = function () {
  this.return();
}

%AsyncIteratorPrototype%[email protected]@asyncDispose()

We propose to add @@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 @@dispose to such objects as the return value from Proxy.revocable(), but that is currently out of scope for the current proposal.

Examples

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

WHATWG Streams API

try using (const reader = stream.getReader()) {
  const { value, done } = reader.read();
  ...
}
// or
{
  using const reader = stream.getReader();
  const { value, done } = reader.read();
}

NodeJS FileHandle

try using (const 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);
} // both handles are closed

// or
{
  using const f1 = fs.promises.open(s1, constants.O_RDONLY),
              f2 = fs.promises.open(s2, constants.O_WRONLY);
  const buffer = Buffer.alloc(4092);
  const { bytesRead } = await f1.read(buffer);
  await f2.write(buffer, 0, bytesRead);
} // both handles are closed

Transactional Consistency (ACID)

// roll back transaction if either action fails
try using (const tx = transactionManager.startTransaction(account1, account2)) {
  await account1.debit(amount);
  await account2.credit(amount);

  // mark transaction success
  tx.succeeded = true;
} // transaction is committed

// or
{
  using const tx = transactionManager.startTransaction(account1, account2);
  await account1.debit(amount);
  await account2.credit(amount);

  // mark transaction success
  tx.succeeded = true;
} // transaction is committed

Logging and tracing

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

// or
function privilegedActivity() {
  using value 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 value await sem.wait(); // asynchronously block until we are the sole participant
  ...
} // synchronously release semaphore and notify the next participant

API

Additions to Symbol

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

interface SymbolConstructor {
  readonly dispose: symbol;
  readonly asyncDispose: symbol;
}

In addition, the methods [Symbol.dispose] and [Symbol.asyncDispose] methods would be added to %GeneratorPrototype% and %AsyncGeneratorPrototype%, respectively. Each method, when called, calls the return method on those prototypes.

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.
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 must return a Promise.
interface AsyncDisposable {
  /**
   * Disposes of resources within this object.
   */
  [Symbol.asyncDispose](): Promise<void>;
}

Disposable and AsyncDisposable container objects

This proposal adds two global objects that can 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, they would be collected and an AggregateError would be thrown at the end:

class Disposable {
  /**
   * @param {Iterable<Disposable>} disposables An iterable containing objects to be disposed 
   * when this object is disposed.
   * @returns {Disposable}
   */
  static from(disposables);

  /**
   * @param {() => void} onDispose A callback to execute when this object is disposed.
   */
  constructor(onDispose);

  /**
   * Disposes of resources within this object.
   */
  [Symbol.dispose]();
}

class AsyncDisposable {
  /**
   * @param {Iterable<Disposable | AsyncDisposable>} disposables An iterable containing objects 
   * to be disposed when this object is disposed.
   */
  static from(disposables);

  /**
   * @param {() => void | Promise<void>} onAsyncDispose A callback to execute when this object is
   * disposed.
   */
  constructor(onAsyncDispose);

  /**
   * Asynchronously disposes of resources within this object.
   * @returns {Promise<void>}
   */
  [Symbol.asyncDispose]();
}

The Disposable class, as well as its async variant, the AsyncDisposable class, each provide two capabilities:

  • Aggregation
  • Interoperation and Customization

Aggregation

The Disposable and AsyncDisposable classes provide the ability to aggregate multiple disposable resources into a single container. When the Disposable container is disposed, each object in the container is also guaranteed to be disposed (barring early termination of the program). Any exceptions thrown as resources in the container are disposed will be collected and rethrown as an AggregateError.

Interoperation and Customization

The Disposable and AsyncDisposable classes also provide the ability to create a disposable resource from a simple callback. This callback will be executed when the resource's Symbol.dispose method (or Symbol.asyncDispose method, for an AsyncDisposable) is executed.

The ability to create a disposable resource from a callback has several benefits:

  • It allows developers to leverage try using (or using const, etc.) while working with existing resources that do not conform to the Symbol.dispose mechanic:
    const reader = ...;
    try using (new Disposable(() => reader.releaseLock())) {
      ...
    }
    // or
    {
      const reader = ...;
      using value new Disposable(() => 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:
    const defer = f => new Disposable(f);
    ...
    function f() {
      console.log("enter");
      using value defer(() => console.log("exit"));
      ...
    }
    

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

  • [x] Identified a "champion" who will advance the addition.
  • [x] Prose outlining the problem or need and the general shape of a solution.
  • [x] Illustrative examples of usage.
  • [x] 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.
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].