- Notifications
You must be signed in to change notification settings - Fork40
ECMAScript Explicit Resource Management
License
tc39/proposal-explicit-resource-management
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
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.
Stage: 3
Champion: Ron Buckton (@rbuckton)
Last Presented: March, 2023 (slides,notes #1,notes #2)
For more information see theTC39 proposal process.
- Ron Buckton (@rbuckton)
This proposal is motivated by a number of cases:
Inconsistent patterns for resource management:
- ECMAScript Iterators:
iterator.return() - WHATWG Stream Readers:
reader.releaseLock() - NodeJS FileHandles:
handle.close() - Emscripten C++ objects handles:
Module._free(ptr) obj.delete() Module.destroy(obj)
- ECMAScript Iterators:
Avoiding common footguns when managing resources:
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 and
shared 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'}
- 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:
WeakMapkeysWeakSetvaluesWeakRefvaluesFinalizationRegistryentries
- Explicit Resource Management — Indicates a system whereby the lifetime of a "resource" is managed explicitlyby the user eitherimperatively (by directly calling a method like
Symbol.dispose) ordeclaratively (througha block-scoped declaration likeusing).
// a synchronously-disposed, block-scoped resourceusingx=expr1;// resource w/ local bindingusingy=expr2,z=expr4;// multiple resources
Please refer to thespecification text for the most recent version of the grammar.
// 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 a
for-oforfor-await-ofstatement.
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:
| 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 |
UsingDeclaration : `using` BindingList `;`LexicalBinding : BindingIdentifier InitializerWhen 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.
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.
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
If a resource does not have a callable[Symbol.dispose] member, aTypeError would be thrownimmediately when theresource is tracked.
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.
UsingDeclaration : `await` `using` BindingList `;`LexicalBinding : BindingIdentifier InitializerWhen 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.
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.
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
If a resource does not have a callable[Symbol.asyncDispose] or[Symbol.asyncDispose] member, aTypeError would be thrownimmediately when the resource is tracked.
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.
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.
The following show examples of using this proposal with various APIs, assuming those APIs adopted this proposal.
{usingreader=stream.getReader();const{ value, done}=reader.read();}// 'reader' is disposed
{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
{awaitusingwritable= ...;writable.write(...);}// 'writable.end()' is called and its result is awaited
// audit privileged function call entry and exitfunctionprivilegedActivity(){usingactivity=auditLog.startActivity("privilegedActivity");// log activity start ...}// log activity end
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
// 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
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
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;}
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:
AggregateErroris designed to hold a list of multiple errors, with no correlation between those errors, whileSuppressedErroris intended to hold references to two errors with a direct correlation.AggregateErroris intended to ideally hold a flat list of errors.SuppressedErroris intended to hold a jagged setof errors (i.e.,e.suppressed.suppressed.suppressedif there were successive error suppressions).- The only error correlation on
AggregateErroris throughcause, however aSuppressedErrorisn't "caused" by theerror it suppresses. In addition,causeis intended to be optional, while theerrorof aSuppressedErrormustalways be defined.
We also propose to addSymbol.dispose to the built-in%IteratorPrototype% as if it had the following behavior:
%IteratorPrototype%[Symbol.dispose]=function(){this.return();}
We propose to addSymbol.asyncDispose to the built-in%AsyncIteratorPrototype% as if it had the following behavior:
%AsyncIteratorPrototype%[Symbol.asyncDispose]=asyncfunction(){awaitthis.return();}
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.
An object isdisposable if it conforms to the following interface:
| Property | Value | Requirements |
|---|---|---|
@@dispose | A function that performs explicit cleanup. | The function should returnundefined. |
TypeScript Definition
interfaceDisposable{/** * Disposes of resources within this object. */[Symbol.dispose]():void;}
An object isasync disposable if it conforms to the following interface:
| Property | Value | Requirements |
|---|---|---|
@@asyncDispose | An async function that performs explicit cleanup. | The function should return aPromise. |
TypeScript Definition
interfaceAsyncDisposable{/** * Disposes of resources within this object. */[Symbol.asyncDispose]():Promise<void>;}
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:
DisposableStackis inspired by Python'sExitStack.
NOTE:
AsyncDisposableStackis inspired by Python'sAsyncExitStack.
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))
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 leverage
using/await usingwhile working with existing resources that do not conform to theSymbol.dispose/Symbol.asyncDisposemechanic:{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's
deferstatement:functionf(){usingstack=newDisposableStack();console.log("enter");stack.defer(()=>console.log("exit")); ...}
A user-defined disposable class might need to allocate and track multiple nested resources that should be disposed 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]();}}}
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.
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..ofimplies iteration, which can be confusing when reading code.- Conflating
for..ofand resource management could make it harder to find documentation, examples, StackOverflowanswers, etc. - A
for..ofimplementation 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 to
using. - Mandates introduction of a new block scope, even at the top level of a function body.
- Control flow analysis of a
for..ofloop 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.}
- Using
continueandbreakis 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; ...}}
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.
- NOTE:
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.
- NOTE:
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.
- NOTE:
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(), orResizeObserver—@@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.
- NOTE:
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 toaddEventListenercouldreturn aDisposablethat removes the event listener when disposed.Performance.prototype.measureBlock(measureName, options) -> Disposable— Combinesmarkandmeasureinto 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().
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).
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 with
ref()andunref()methods — A new method or API that produces asingle-use disposer forref()andunref(). - Anything with
cork()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@@asyncDisposeonly 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 in
net,readline,tls,udp, andworker_threads.
- TC39 July 24th, 2018
- Conclusion
- Stage 1 acceptance
- Conclusion
- TC39 July 23rd, 2019
- Conclusion
- Table until Thursday, inconclusive.
- Conclusion
- TC39 July 25th, 2019
- Conclusion:
- Investigate Syntax
- Approved for Stage 2
- YK (@wycatz) & WH (@waldemarhorwat) will be stage 3 reviewers
- Conclusion:
- TC39 October 10th, 2021
- Conclusion
- Status Update only
- WH Continuing to review
- SYG (@syg) added as reviewer
- Conclusion
- TC39 December 1st, 2022
- Conclusion
usingdeclarations,Symbol.dispose, andDisposableStackadvanced to Stage 3, under the following conditions:- Resolution of#103 - Argument order for
adopt() - Deferral of
async usingdeclarations,Symbol.asyncDispose, andAsyncDisposableStack.
- Resolution of#103 - Argument order for
- async
usingdeclarations,Symbol.asyncDispose, andAsyncDisposableStackremain at Stage 2 as an independentproposal.
- Conclusion
- TC39 January 31st, 2023
- Conclusion
- Ban
awaitas identifier inusing(#138) was accepted - Support
usingat top level ofeval(#136) was rejected- May consider a needs-consensus PR in the future based on implementer/community feedback.
- Ban
- Conclusion
- TC39 February 1st, 2023
- Conclusion
- Rename
Symbol.asyncDisposetoSymbol.disposeAsyncwas rejected - Conditional advancement to Stage 3 at March 2023 plenary pending outcome of investigation into
async usingvs.using awaitsyntax.
- Rename
- Conclusion
- TC39 March 21st, 2023
- Conclusion
- Committee resolves to adopt
await usingpending investigation of potential cover grammar.
- Committee resolves to adopt
- Conclusion
- TC39 March 23rd, 2023
- Conclusion
- Stage 3, conditionally on final review of cover grammar by Waldemar Horwat.
- Consensus on normative change to remove
awaitidentifier restriction forusingdeclarations.
- Conclusion
The following is a high-level list of tasks to progress through each stage of theTC39 proposal process:
- 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.
- Initial specification text.
- Transpiler support (Optional).
- Complete specification text.
- Designated reviewers have signed off on the current spec text:
- TheECMAScript editor hassigned off on the current spec text.
- 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.
- Built-ins from this proposal are available in
core-js
About
ECMAScript Explicit Resource Management
Resources
License
Code of conduct
Contributing
Security policy
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Uh oh!
There was an error while loading.Please reload this page.