Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

ECMAScript Explicit Resource Management

License

NotificationsYou must be signed in to change notification settings

tc39/proposal-explicit-resource-management

NOTE: This proposal has subsumed theAsync Explicit Resource Managementproposal. This proposal repository should be used for further discussion of both sync and async of explicit resourcemanagement.

This proposal intends to address a common pattern in software development regardingthe lifetime and management of various resources (memory, I/O, etc.). This patterngenerally includes the allocation of a resource and the ability to explicitlyrelease critical resources.

For example, ECMAScript Generator Functions and Async Generator Functions expose this pattern through thereturn method, as a means to explicitly evaluatefinally blocks to ensureuser-defined cleanup logic is preserved:

// sync generatorsfunction*g(){consthandle=acquireFileHandle();// critical resourcetry{    ...}finally{handle.release();// cleanup}}constobj=g();try{constr=obj.next();  ...}finally{obj.return();// calls finally blocks in `g`}
// async generatorsasyncfunction*g(){consthandle=acquireStream();// critical resourcetry{    ...}finally{awaitstream.close();// cleanup}}constobj=g();try{constr=awaitobj.next();  ...}finally{awaitobj.return();// calls finally blocks in `g`}

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

// sync disposalfunction*g(){usinghandle=acquireFileHandle();// block-scoped critical resource}// cleanup{usingobj=g();// block-scoped declarationconstr=obj.next();}// calls finally blocks in `g`
// async disposalasyncfunction*g(){usingstream=acquireStream();// block-scoped critical resource  ...}// cleanup{awaitusingobj=g();// block-scoped declarationconstr=awaitobj.next();}// calls finally blocks in `g`

In addition, we propose the addition of two disposable container objects to assistwith 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 theTC39 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:

    constreader=stream.getReader();...reader.releaseLock();// Oops, should have been in a try/finally
  • Scoping resources:

    consthandle= ...;try{  ...// ok to use `handle`}finally{handle.close();}// not ok to use `handle`, but still in scope
  • Avoiding common footguns when managing multiple resources:

    consta= ...;constb= ...;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 scopeconsta= ...;try{constb= ...;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` throwsusinga= ...,b= ...;...
    // async sync disposal{// block avoids leaking `a` or `b` to outer scopeconsta= ...;try{constb= ...;try{      ...}finally{awaitb.close();// ensure `b` is closed before `a` in case `b`// depends on `a`}}finally{awaita.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` throwsawaitusinga= ...,b= ...;...
  • Non-blocking memory/IO applications:

    import{ReaderWriterLock}from"...";constlock=newReaderWriterLock();exportasyncfunctionreadData(){// wait for outstanding writer and take a read lockusinglockHandle=awaitlock.read();  ...// any number of readersawait ...;  ...// still in read lock after `await`}// release the read lockexportasyncfunctionwriteData(data){// wait for all readers and take a write lockusinglockHandle=awaitlock.write();  ...// only one writerawait ...;  ...// still in write lock after `await`}// release the write lock
  • Potential for use with theFixed Layout Objects Proposal andshared struct:

    // main.jssharedstructclassSharedData{ready=false;processed=false;}constworker=newWorker('worker.js');constm=newAtomics.Mutex();constcv=newAtomics.ConditionVariable();constdata=newSharedData();worker.postMessage({ m, cv, data});// send data to worker{// wait until main can get a lock on 'm'usinglck=m.lock();// mark data for workerdata.ready=true;console.log("main is ready");}// unlocks 'm'// notify potentially waiting workercv.notifyOne();{// reacquire lock on 'm'usinglck=m.lock();// release the lock on 'm' and wait for the worker to finish processingcv.wait(m,()=>data.processed);}// unlocks 'm'
    // worker.jsonmessage=function(e){const{ m, cv, data}=e.data;{// wait until worker can get a lock on 'm'usinglck=m.lock();// release the lock on 'm' and wait until main() sends datacv.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 maindata.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 operationshould be performed or a non-garbage-collected reference (such as a file handle, socket, etc.) should be closed orfreed.
  • Resource Management — A process whereby "resources" are released, triggering any lifetime-sensitive operationsor freeing any related non-garbage-collected references.
  • Implicit Resource Management — Indicates a system whereby the lifetime of a "resource" is managed implicitlyby 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 explicitlyby the user eitherimperatively (by directly calling a method likeSymbol.dispose) ordeclaratively (througha block-scoped declaration likeusing).

Syntax

using Declarations

// a synchronously-disposed, block-scoped resourceusingx=expr1;// resource w/ local bindingusingy=expr2,z=expr4;// multiple resources

Grammar

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

await using Declarations

// an asynchronously-disposed, block-scoped resourceawaitusingx=expr1;// resource w/ local bindingawaitusingy=expr2,z=expr4;// multiple resources

Anawait using declaration can appear in the following contexts:

  • The top level of aModule anywhereVariableStatement is allowed, as long as it is not immediately nested insideof aCaseClause orDefaultClause.
  • In the body of an async function or async generator anywhere aVariableStatement is allowed, as long as it is notimmediately nested inside of aCaseClause orDefaultClause.
  • In the head of afor-of orfor-await-of statement.

await using infor-of andfor-await-of Statements

for(awaitusingxofy) ...forawait(awaitusingxofy)...

You can use anawait using declaration in afor-of orfor-await-of statement inside of an async context toexplicitly bind each iterated value as an async disposable resource.for-await-of does not implicitly make a non-asyncusing declaration into an asyncawait using declaration, as theawait markers infor-await-of andawait usingare explicit indicators for distinct cases:for awaitonly indicates async iteration, whileawait usingonlyindicates async disposal. For example:

// sync iteration, sync disposalfor(usingxofy);// no implicit `await` at end of each iteration// sync iteration, async disposalfor(awaitusingxofy);// implicit `await` at end of each iteration// async iteration, sync disposalforawait(usingxofy);// implicit `await` at end of each iteration// async iteration, async disposalforawait(awaitusingxofy);// implicit `await` at end of each iteration

While there is some overlap in that the last three cases introduce some form of implicitawait during execution, itis intended that the presence or absence of theawait modifier in ausing declaration is an explicit indicator as towhether we are expecting the iterated value to have an@@asyncDispose method. This distinction is in line with thebehavior offor-of andfor-await-of:

constiter={[Symbol.iterator](){return[].values();}};constasyncIter={[Symbol.asyncIterator](){return[].values();}};for(constxofiter);// ok: `iter` has @@iteratorfor(constxofasyncIter);// throws: `asyncIter` does not have @@iteratorforawait(constxofiter);// ok: `iter` has @@iterator (fallback)forawait(constxofasyncIter);// ok: `asyncIter` has @@asyncIterator

using andawait using have the same distinction:

constres={[Symbol.dispose](){}};constasyncRes={[Symbol.asyncDispose](){}};usingx=res;// ok: `res` has @@disposeusingx=asyncRes;// throws: `asyncRes` does not have @@disposeawaitusingx=res;// ok: `res` has @@dispose (fallback)awaitusingx=asyncres;// ok: `asyncRes` has @@asyncDispose

This results in a matrix of behaviors based on the presence of eachawait marker:

constres={[Symbol.dispose](){}};constasyncRes={[Symbol.asyncDispose](){}};constiter={[Symbol.iterator](){return[res,asyncRes].values();}};constasyncIter={[Symbol.asyncIterator](){return[res,asyncRes].values();}};for(usingxofiter);// sync iteration, sync disposal// - `iter` has @@iterator: ok// - `res` has @@dispose: ok// - `asyncRes` does not have @@dispose: *error*for(usingxofasyncIter);// sync iteration, sync disposal// - `asyncIter` does not have @@iterator: *error*for(awaitusingxofiter);// sync iteration, async disposal// - `iter` has @@iterator: ok// - `res` has @@dispose (fallback): ok// - `asyncRes` has @@asyncDispose: okfor(awaitusingxofasyncIter);// sync iteration, async disposal// - `asyncIter` does not have @@iterator: errorforawait(usingxofiter);// async iteration, sync disposal// - `iter` has @@iterator (fallback): ok// - `res` has @@dispose: ok// - `asyncRes` does not have @@dispose: errorforawait(usingxofasyncIter);// async iteration, sync disposal// - `asyncIter` has @@asyncIterator: ok// - `res` has @@dispose: ok// - `asyncRes` does not have @@dispose: errorforawait(awaitusingxofiter);// async iteration, async disposal// - `iter` has @@iterator (fallback): ok// - `res` has @@dispose (fallback): ok// - `asyncRes` does has @@asyncDispose: okforawait(awaitusingxofasyncIter);// async iteration, async disposal// - `asyncIter` has @@asyncIterator: ok// - `res` has @@dispose (fallback): ok// - `asyncRes` does has @@asyncDispose: ok

Or, in table form:

SyntaxIterationDisposal
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 ausing declaration is parsed withBindingIdentifierInitializer, the bindings created in the declarationare tracked for disposal at the end of the containingBlock orModule (ausing declaration cannot be usedat the top level of aScript):

{  ...// (1)usingx=expr1;  ...// (2)}

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

{const$$try={stack:[],error:undefined,hasError:false};try{    ...// (1)constx=expr1;if(x!==null&&x!==undefined){const$$dispose=x[Symbol.dispose];if(typeof$$dispose!=="function"){thrownewTypeError();}$$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 ?newSuppressedError($$error,$$try.error) :$$error;$$try.hasError=true;}}if($$try.hasError){throw$$try.error;}}}

If exceptions are thrown both in the block following theusing declaration and in the call to[Symbol.dispose](), all exceptions are reported.

using Declarations with Multiple Resources

Ausing declaration can mix multiple explicit bindings in the same declaration:

{  ...usingx=expr1,y=expr2;  ...}

These bindings are again used to perform resource disposal when theBlock orModule exits, however in this case[Symbol.dispose]() is invoked in the reverse order of their declaration. This isapproximately equivalent to thefollowing:

{  ...// (1)usingx=expr1;usingy=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)constx=expr1;if(x!==null&&x!==undefined){const$$dispose=x[Symbol.dispose];if(typeof$$dispose!=="function"){thrownewTypeError();}$$try.stack.push({value:x,dispose:$$dispose});}consty=expr2;if(y!==null&&y!==undefined){const$$dispose=y[Symbol.dispose];if(typeof$$dispose!=="function"){thrownewTypeError();}$$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 ?newSuppressedError($$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 mightoccur during binding initialization results in evaluation of the cleanup step. When there are multiple declarations inthe list, we track each resource in the order they are declared. As a result, we must release these resources in reverseorder.

using Declarations andnull orundefined Values

This proposal has opted to ignorenull andundefined values provided to theusing declarations. This is similar tothe behavior ofusing in C#, which also allowsnull. One primary reason for this behavior is to simplify a commoncase where a resource might be optional, without requiring duplication of work or needless allocations:

if(isResourceAvailable()){usingresource=getResource();  ...// (1)resource.doSomething()...// (2)}else{// duplicate code path above  ...// (1) above  ...// (2) above}

Compared to:

usingresource=isResourceAvailable() ?getResource() :undefined;...// (1) do some work with or without resourceresource?.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, aTypeError would be thrownimmediately when theresource is tracked.

using Declarations infor-of andfor-await-of Loops

Ausing declarationmay occur in theForDeclaration of afor-of orfor-await-of loop:

for(usingxofiterateResources()){// use x}

In this case, the value bound tox in each iteration will besynchronously disposed at the end of each iteration.This will not dispose resources that are not iterated, such as if iteration is terminated early due toreturn,break, orthrow.

using declarationsmay not be used in in the head of afor-in loop.

await using Declarations

await using Declarations with Explicit Local Bindings

UsingDeclaration :  `await` `using` BindingList `;`LexicalBinding :    BindingIdentifier Initializer

When anawait using declaration is parsed withBindingIdentifierInitializer, the bindings created in thedeclaration are tracked for disposal at the end of the containing async function body,Block, orModule:

{  ...// (1)awaitusingx=expr1;  ...// (2)}

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

{const$$try={stack:[],error:undefined,hasError:false};try{    ...// (1)constx=expr1;if(x!==null&&x!==undefined){let$$dispose=x[Symbol.asyncDispose];if(typeof$$dispose!=="function"){$$dispose=x[Symbol.dispose];}if(typeof$$dispose!=="function"){thrownewTypeError();}$$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 ?newSuppressedError($$error,$$try.error) :$$error;$$try.hasError=true;}}if($$try.hasError){throw$$try.error;}}}

If exceptions are thrown both in the statements following theawait using declaration and in the call to[Symbol.asyncDispose](), all exceptions are reported.

await using Declarations with Multiple Resources

Anawait using declaration can mix multiple explicit bindings in the same declaration:

{  ...awaitusingx=expr1,y=expr2;  ...}

These bindings are again used to perform resource disposal when theBlock orModule exits, however in this case eachresource's[Symbol.asyncDispose]() is invoked in the reverse order of their declaration. This isapproximatelyequivalent to the following:

{  ...// (1)awaitusingx=expr1;awaitusingy=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)constx=expr1;if(x!==null&&x!==undefined){let$$dispose=x[Symbol.asyncDispose];if(typeof$$dispose!=="function"){$$dispose=x[Symbol.dispose];}if(typeof$$dispose!=="function"){thrownewTypeError();}$$try.stack.push({value:x,dispose:$$dispose});}consty=expr2;if(y!==null&&y!==undefined){let$$dispose=y[Symbol.asyncDispose];if(typeof$$dispose!=="function"){$$dispose=y[Symbol.dispose];}if(typeof$$dispose!=="function"){thrownewTypeError();}$$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 ?newSuppressedError($$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 mightoccur during binding initialization results in evaluation of the cleanup step. When there are multiple declarations inthe list, we track each resource in the order they are declared. As a result, we must release these resources in reverseorder.

await using Declarations andnull orundefined Values

This proposal has opted to ignorenull andundefined values provided toawait using declarations. This isconsistent with the proposed behavior for theusing declarations in this proposal. Like in the sync case, this allowssimplifying a common case where a resource might be optional, without requiring duplication of work or needlessallocations:

if(isResourceAvailable()){awaitusingresource=getResource();  ...// (1)resource.doSomething()...// (2)}else{// duplicate code path above  ...// (1) above  ...// (2) above}

Compared to:

awaitusingresource=isResourceAvailable() ?getResource() :undefined;...// (1) do some work with or without resourceresource?.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, aTypeError would be thrownimmediately when the resource is tracked.

await using Declarations infor-of andfor-await-of Loops

Anawait using declarationmay occur in theForDeclaration of afor-await-of loop:

forawait(awaitusingxofiterateResources()){// use x}

In this case, the value bound tox in each iteration will beasynchronously disposed at the end of each iteration.This will not dispose resources that are not iterated, such as if iteration is terminated early due toreturn,break, orthrow.

await using declarationsmay not be used in in the head of afor-of orfor-in loop.

Implicit Async Interleaving Points ("implicitawait")

Theawait using syntax introduces an implicit async interleaving point (i.e., an implicitawait) whenever controlflow exits an async function body,Block, orModule containing anawait using declaration. This means that twostatements that currently execute in the same microtask, such as:

asyncfunctionf(){{a();}// exit blockb();// same microtask as call to `a()`}

will instead execute in different microtasks if anawait using declaration is introduced:

asyncfunctionf(){{awaitusingx= ...;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 thatthe presence ofawait using within such a block is an adequate indicator, since it should be fairly easy to recognizeaBlock containing anawait using statement in well-formatted code.

It is also feasible for editors to use features such as syntax highlighting, editor decorations, and inlay hints tofurther highlight such transitions, without needing to specify additional syntax.

Further discussion around theawait using syntax and how it pertains to implicit async interleaving points can befound in#1.

Examples

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

WHATWG Streams API

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

NodeJS FileHandle

{usingf1=awaitfs.promises.open(s1,constants.O_RDONLY),f2=awaitfs.promises.open(s2,constants.O_WRONLY);constbuffer=Buffer.alloc(4092);const{ bytesRead}=awaitf1.read(buffer);awaitf2.write(buffer,0,bytesRead);}// 'f2' is disposed, then 'f1' is disposed

NodeJS Streams

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

Logging and tracing

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

Async Coordination

import{Semaphore}from"...";constsem=newSemaphore(1);// allow one participant at a timeexportasyncfunctiontryUpdate(record){usinglck=awaitsem.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 failsasyncfunctiontransfer(account1,account2){awaitusingtx=transactionManager.startTransaction(account1,account2);awaitaccount1.debit(amount);awaitaccount2.credit(amount);// mark transaction success if we reach this pointtx.succeeded=true;}// await transaction commit or rollback

Shared Structs

main_thread.js

// main_thread.jssharedstructData{mut;cv;ready=0;processed=0;// ...}constdata=Data();data.mut=Atomics.Mutex();data.cv=Atomics.ConditionVariable();// start two workersstartWorker1(data);startWorker2(data);

worker1.js

constdata= ...;const{ mut, cv}=data;{// lock mutexusinglck=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 wakeAtomics.ConditionVariable.notifyOne(cv);{// reacquire lock on mutexusinglck=Atomics.Mutex.lock(mut);// NOTE: at this point we currently own the lock// release mutex and wait until condition is met to reacquire itAtomics.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

constdata= ...;const{ mut, cv}=data;{// lock mutexusinglck=Atomics.Mutex.lock(mut);// NOTE: at this point we currently own the lock// release mutex and wait until condition is met to reacquire itAtomics.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 toSymbol

This proposal adds thedispose andasyncDispose properties to theSymbol 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 ofusing declarations and byDisposableStack objects.
@@asyncDispose"Symbol.asyncDispose"A method that asynchronosly explicitly disposes of resources held by the object. Called by the semantics ofawait using declarations and byAsyncDisposableStack objects.

TypeScript Definition

interfaceSymbolConstructor{readonlyasyncDispose:unique symbol;readonlydispose:unique symbol;}

TheSuppressedError Error

If an exception occurs during resource disposal, it is possible that it might suppress an existing exception thrownfrom the body, or from the disposal of another resource. Languages like Java allow you to access a suppressed exceptionvia agetSuppressed() method onthe exception. However, ECMAScript allows you to throw any value, not justError, so there is no convenient place toattach a suppressed exception. To better surface these suppressed exceptions and support both logging and errorrecovery, this proposal seeks to introduce a newSuppressedError built-inError subclass which would contain boththe error that was most recently thrown, as well as the error that was suppressed:

classSuppressedErrorextendsError{/**   * 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 useSuppressedError overAggregateError for several reasons:

  • AggregateError is designed to hold a list of multiple errors, with no correlation between those errors, whileSuppressedError is intended to hold references to two errors with a direct correlation.
  • AggregateError is intended to ideally hold a flat list of errors.SuppressedError is intended to hold a jagged setof errors (i.e.,e.suppressed.suppressed.suppressed if there were successive error suppressions).
  • The only error correlation onAggregateError is throughcause, however aSuppressedError isn't "caused" by theerror it suppresses. In addition,cause is intended to be optional, while theerror of aSuppressedError mustalways be defined.

Built-in Disposables

%IteratorPrototype%.@@dispose()

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

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

%AsyncIteratorPrototype%.@@asyncDispose()

We propose to addSymbol.asyncDispose to the built-in%AsyncIteratorPrototype% as if it had the following behavior:

%AsyncIteratorPrototype%[Symbol.asyncDispose]=asyncfunction(){awaitthis.return();}

Other Possibilities

We could also consider addingSymbol.dispose to such objects as the return value fromProxy.revocable(), but thatis currently out of scope for the current proposal.

The CommonDisposable andAsyncDisposable Interfaces

TheDisposable Interface

An object isdisposable if it conforms to the following interface:

PropertyValueRequirements
@@disposeA function that performs explicit cleanup.The function should returnundefined.

TypeScript Definition

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

TheAsyncDisposable Interface

An object isasync disposable if it conforms to the following interface:

PropertyValueRequirements
@@asyncDisposeAn async function that performs explicit cleanup.The function should return aPromise.

TypeScript Definition

interfaceAsyncDisposable{/**   * Disposes of resources within this object.   */[Symbol.asyncDispose]():Promise<void>;}

TheDisposableStack andAsyncDisposableStack container objects

This proposal adds two global objects that can act as containers to aggregate disposables, guaranteeing that everydisposable resource in the container is disposed when the respective disposal method is called. If any disposable in thecontainer throws an error during dispose, it would be thrown at the end (possibly wrapped in aSuppressedError ifmultiple errors were thrown):

classDisposableStack{constructor();/**   * Gets a value indicating whether the stack has been disposed.   *@returns {boolean}   */getdisposed();/**   * 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 ofDisposableStack 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 rejectedPromise, it would bethrown at the end (possibly wrapped in aSuppressedError 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'sExitStack.

NOTE:AsyncDisposableStack is inspired by Python'sAsyncExitStack.

Aggregation

TheDisposableStack andAsyncDisposableStack classes provid the ability to aggregate multiple disposable resourcesinto a single container. When theDisposableStack container is disposed, each object in the container is alsoguaranteed 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 wrappedin nestedSuppressedError objects.

For example:

// syncconststack=newDisposableStack();constresource1=stack.use(getResource1());constresource2=stack.use(getResource2());constresource3=stack.use(getResource3());stack[Symbol.dispose]();// disposes of resource3, then resource2, then resource1
// asyncconststack=newAsyncDisposableStack();constresource1=stack.use(getResource1());constresource2=stack.use(getResource2());constresource3=stack.use(getResource3());awaitstack[Symbol.asyncDispose]();// dispose and await disposal result of resource3, then resource2, then resource1

If all ofresource1,resource2 andresource3 were to throw during disposal, this would produce an exceptionsimilar to the following:

newSuppressedError(/*error*/exception_from_resource3_disposal,/*suppressed*/newSuppressedError(/*error*/exception_from_resource2_disposal,/*suppressed*/exception_from_resource1_disposal))

Interoperation and Customization

TheDisposableStack andAsyncDisposableStack classes also provide the ability to create a disposable resource from asimple 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 leverageusing/await using while working with existing resources that do not conform to theSymbol.dispose/Symbol.asyncDispose mechanic:
    {usingstack=newDisposableStack();constreader=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'sdefer statement:
    functionf(){usingstack=newDisposableStack();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 whenthe class instance is disposed. However, properly managing the lifetime of these nested resources in the classconstructor can sometimes be difficult. Themove method ofDisposableStack/AsyncDisposableStack helps to moreeasily manage lifetime in these scenarios:

// syncclassPluginHost{  #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.usingstack=newDisposableStack();// Create an IPC adapter around process.send/process.on("message").// When disposed, it unsubscribes from process.on("message").this.#channel=stack.use(newNodeProcessIpcChannelAdapter(process));// Create a pseudo-websocket that sends and receives messages over// a NodeJS IPC channel.this.#socket=stack.use(newNodePluginHostIpcSocket(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)thrownewReferenceError("Object is disposed.");// ...}[Symbol.dispose](){if(!this.#disposed){this.#disposed=true;constdisposables=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]();}}}
// asyncconstprivateConstructorSentinel={};classAsyncPluginHost{  #disposed=false;  #disposables;  #channel;  #socket;/**@private */constructor(arg){if(arg!==privateConstructorSentinel)thrownewTypeError("Use AsyncPluginHost.create() instead");}// NOTE: there's no such thing as an async constructorstaticasynccreate(){consthost=newAsyncPluginHost(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.awaitusingstack=newAsyncDisposableStack();// Create an IPC adapter around process.send/process.on("message").// When disposed, it unsubscribes from process.on("message").host.#channel=stack.use(newNodeProcessIpcChannelAdapter(process));// Create a pseudo-websocket that sends and receives messages over// a NodeJS IPC channel.host.#socket=stack.use(newNodePluginHostIpcSocket(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.returnhost;}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)thrownewReferenceError("Object is disposed.");// ...}async[Symbol.asyncDispose](){if(!this.#disposed){this.#disposed=true;constdisposables=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`awaitdisposables[Symbol.asyncDispose]();}}}

SubclassingDisposable Classes

You can also use aDisposableStack to assist with disposal in a subclass constructor whose superclass is disposable:

classDerivedPluginHostextendsPluginHost{constructor(){super();// Create a DisposableStack to cover the subclass constructor.usingstack=newDisposableStack();// 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 freeddoSomethingThatCouldPotentiallyThrow();// As the last step before exiting, empty out the DisposableStack so that we don't dispose ourselves.stack.move();}}

Here, we can usestack to track the result ofsuper() (i.e., thethis value). If any exception occurs duringsubclass construction, we can ensure that[Symbol.dispose]() is called, freeing resources. If the subclass also needsto track its own disposable resources, this example is modified slightly:

classDerivedPluginHostWithOwnDisposablesextendsPluginHost{  #logger;  #disposables;constructor(){super()// Create a DisposableStack to cover the subclass constructor.usingstack=newDisposableStack();// 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(newFileLogger());// If any operations throw during subclass construction, the instance will still be disposed, and superclass// resources will be freeddoSomethingThatCouldPotentiallyThrow();// 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 thestack and move its contents into the subclass instance'sthis.#disposables. In the subclass[Symbol.dispose]() method we don't need to callsuper[Symbol.dispose]() sincethat has already been tracked by thestack.defer call in the constructor.

Relation toIterator andfor..of

Iterators in ECMAScript also employ a "cleanup" step by way of supplying areturn method. This means that there issome similarity between ausing declaration and afor..of statement:

// usingfunctionf(){usingx= ...;// use x}// x is disposed// for..offunctionmakeDisposableScope(){constresources=[];letstate=0;return{next(){switch(state){case0:state++;return{done:false,value:{use(value){resources.unshift(value);returnvalue;}}};case1:state++;for(constvalueofresources){value?.[Symbol.dispose]();}default:state=-1;return{done:true};}},return(){switch(state){case1:state++;for(constvalueofresources){value?.[Symbol.dispose]();}default:state=-1;return{done:true};}},[Symbol.iterator](){returnthis;}}}functionf(){for(const{ use}ofmakeDisposableScope()){constx=use(...);// use x}// x is disposed}

However there are a number drawbacks to usingfor..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.
  • Conflatingfor..of and resource management could make it harder to find documentation, examples, StackOverflowanswers, etc.
  • Afor..of implementation like the one above cannot control the scope ofuse, which can make lifetimes confusing:
    for(const{ use}of ...){constx=use(...);// oksetImmediate(()=>{consty=use(...);// wrong lifetime});}
  • Significantly more boilerplate compared tousing.
  • Mandates introduction of a new block scope, even at the top level of a function body.
  • Control flow analysis of afor..of loop cannot infer definite assignment since a loop could potentially have zeroelements:
    // usingfunctionf1(){/**@type {string | undefined} */letx;{usingy= ...;x=y.text;}x.toString();// x is definitely assigned}// for..offunctionf2(){/**@type {string | undefined} */letx;for(const{ use}of ...){consty=use(...);x=y.text;}x.toString();// possibly an error in a static analyzer since `x` is not guaranteed to have been assigned.}
  • Usingcontinue andbreak is more difficult if you need to dispose of an iterated value:
    // usingfor(usingxofiterable){if(!x.ready)continue;if(x.done)break;  ...}// for..ofouter:for(constxofiterable){for(const{ use}of ...){use(x);if(!x.ready)continueouter;if(!x.done)breakouter;    ...}}

Relation to DOM APIs

This proposal does not necessarily require immediate support in the HTML DOM specification, as existing APIs can stillbe adapted by usingDisposableStack orAsyncDisposableStack. However, there are a number of APIs that could benefitfrom this proposal and should be considered by the relevant standards bodies. The following is by no means a completelist, and primarily offers suggestions for consideration. The actual implementation is at the discretion of the relevantstandards bodies.

  • AudioContext@@asyncDispose() as an alias orwrapper forclose().
    • NOTE:close() here is asynchronous, but uses the same name as similar synchronous methods on other objects.
  • BroadcastChannel@@dispose() as an alias orwrapper forclose().
  • EventSource@@dispose() as an alias orwrapper forclose().
  • FileReader@@dispose() as an alias orwrapper forabort().
  • IDbTransaction@@dispose() could invokeabort() if the transaction is still in the active state:
    {usingtx=db.transaction(storeNames);// ...if(...)thrownewError();// ...tx.commit();}// implicit tx.abort() if we don't reach the explicit tx.commit()
  • ImageBitmap@@dispose() as an alias orwrapper forclose().
  • IntersectionObserver@@dispose() as an alias orwrapper fordisconnect().
  • MediaKeySession@@asyncDispose() as an alias orwrapper forclose().
    • NOTE:close() here is asynchronous, but uses the same name as similar synchronous methods on other objects.
  • MessagePort@@dispose() as an alias orwrapper forclose().
  • MutationObserver@@dispose() as an alias orwrapper fordisconnect().
  • PaymentRequest@@asyncDispose() could invokeabort() if the payment is still in the active state.
    • NOTE:abort() here is asynchronous, but uses the same name as similar synchronous methods on other objects.
  • PerformanceObserver@@dispose() as an alias orwrapper fordisconnect().
  • PushSubscription@@asyncDispose() as an alias orwrapper forunsubscribe().
  • ReadableStream@@asyncDispose() as an alias orwrapper forcancel().
  • ReadableStreamDefaultReader — Either@@dispose() as an alias orwrapper forreleaseLock(), or@@asyncDispose() as awrapper forcancel() (but probably not both).
  • RTCPeerConnection@@dispose() as an alias orwrapper forclose().
  • RTCRtpTransceiver@@dispose() as an alias orwrapper forstop().
  • ReadableStreamDefaultController@@dispose() as an alias orwrapper forclose().
  • ReadableStreamDefaultReader — Either@@dispose() as an alias orwrapper forreleaseLock(), or
  • ResizeObserver@@dispose() as an alias orwrapper fordisconnect().
  • ServiceWorkerRegistration@@asyncDispose() as awrapper forunregister().
  • SourceBuffer@@dispose() as awrapper forabort().
  • TransformStreamDefaultController@@dispose() as an alias orwrapper forterminate().
  • WebSocket@@dispose() as awrapper forclose().
  • Worker@@dispose() as an alias orwrapper forterminate().
  • WritableStream@@asyncDispose() as an alias orwrapper forclose().
    • NOTE:close() here is asynchronous, but uses the same name as similar synchronous methods on other objects.
  • WritableStreamDefaultWriter — Either@@dispose() as an alias orwrapper forreleaseLock(), or@@asyncDispose() as awrapper forclose() (but probably not both).
  • XMLHttpRequest@@dispose() as an alias orwrapper forabort().

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

  • EventTarget.prototype.addEventListener(type, listener, { subscription: true }) -> Disposable — An optionpassed toaddEventListener couldreturn aDisposable that removes the event listener when disposed.
  • Performance.prototype.measureBlock(measureName, options) -> Disposable — Combinesmark andmeasure into ablock-scoped disposable:
    functionf(){usingmeasure=performance.measureBlock("f");// marks on entry// ...}// marks and measures on exit
  • SVGSVGElement — A new method producing asingle-use disposer forpauseAnimations() andunpauseAnimations().
  • ScreenOrientation — A new method producing asingle-use disposer forlock() andunlock().

Definitions

Awrapper forx() is a method that invokesx(), but only if the object is in a statesuch that callingx() will not throw as a result of repeated evaluation.

Acallback-adapting wrapper is awrapper that adapts a continuation passing-style methodthat accepts a callback into aPromise-producing method.

Asingle-use disposer forx() andy() indicates a newly constructed disposable objectthat invokesx() when constructed andy() when disposed the first time (and does nothing if the object is disposedmore than once).

Relation to NodeJS APIs

This proposal does not necessarily require immediate support in NodeJS, as existing APIs can still be adapted by usingDisposableStack orAsyncDisposableStack. However, there are a number of APIs that could benefit from this proposaland should be considered by the NodeJS maintainers. The following is by no means a complete list, and primarily offerssuggestions for consideration. The actual implementation is at the discretion of the NodeJS maintainers.

  • Anything withref() andunref() methods — A new method or API that produces asingle-use disposer forref() andunref().
  • Anything withcork() anduncork() methods — A new method or API that produces asingle-use disposer forcork() anduncork().
  • async_hooks.AsyncHook — either@@dispose() as an alias orwrapper fordisable(), or a new method thatproduces asingle-use disposer forenable() anddisable().
  • child_process.ChildProcess@@dispose() as an alias orwrapper forkill().
  • cluster.Worker@@dispose() as an alias orwrapper forkill().
  • crypto.Cipher,crypto.Decipher@@dispose() as awrapper forfinal().
  • crypto.Hash,crypto.Hmac@@dispose() as awrapper fordigest().
  • dns.Resolver,dnsPromises.Resolver@@dispose() as an alias orwrapper forcancel().
  • domain.Domain — A new method or API that produces asingle-use disposer forenter() andexit().
  • events.EventEmitter — A new method or API that produces asingle-use disposer foron() andoff().
  • fs.promises.FileHandle@@asyncDispose() as an alias orwrapper forclose().
  • fs.Dir@@asyncDispose() as an alias orwrapper forclose(),@@dispose() as an alias orwrapperforcloseSync().
  • fs.FSWatcher@@dispose() as an alias orwrapper forclose().
  • http.Agent@@dispose() as an alias orwrapper fordestroy().
  • http.ClientRequest — Either@@dispose() or@@asyncDispose() as an alias orwrapper fordestroy().
  • http.Server@@asyncDispose() as acallback-adapting wrapper forclose().
  • http.ServerResponse@@asyncDispose() as acallback-adapting wrapper forend().
  • http.IncomingMessage — Either@@dispose() or@@asyncDispose() as an alias orwrapper fordestroy().
  • http.OutgoingMessage — Either@@dispose() or@@asyncDispose() as an alias orwrapper fordestroy().
  • http2.Http2Session@@asyncDispose() as acallback-adapting wrapper forclose().
  • http2.Http2Stream@@asyncDispose() as acallback-adapting wrapper forclose().
  • http2.Http2Server@@asyncDispose() as acallback-adapting wrapper forclose().
  • http2.Http2SecureServer@@asyncDispose() as acallback-adapting wrapper forclose().
  • http2.Http2ServerRequest — Either@@dispose() or@@asyncDispose() as an alias orwrapper fordestroy().
  • http2.Http2ServerResponse@@asyncDispose() as acallback-adapting wrapper forend().
  • https.Server@@asyncDispose() as acallback-adapting wrapper forclose().
  • inspector — A new API that produces asingle-use disposer foropen() andclose().
  • stream.Writable — Either@@dispose() or@@asyncDispose() as an alias orwrapper fordestroy() or@@asyncDispose only as acallback-adapting wrapper forend() (depending on whether the disposal behaviorshould be to drop immediately or to flush any pending writes).
  • stream.Readable — Either@@dispose() or@@asyncDispose() as an alias orwrapper fordestroy().
  • ... and many others innet,readline,tls,udp, andworker_threads.

Meeting Notes

TODO

The following is a high-level list of tasks to progress through each stage of theTC39 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.
  • Illustrativeexamples of usage.
  • High-levelAPI.

Stage 2 Entrance Criteria

Stage 3 Entrance Criteria

Stage 4 Entrance Criteria

  • Test262 acceptance tests have been written for mainline usage scenarios andmerged.
  • Two compatible implementations which pass the acceptance tests:[1],[2].
  • Apull request has been sent to tc39/ecma262 with the integrated spec text.
  • The ECMAScript editor has signed off on thepull request.

Implementations

  • Built-ins from this proposal are available incore-js

About

ECMAScript Explicit Resource Management

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks


[8]ページ先頭

©2009-2025 Movatter.jp