Explicit Resource Management S4
- Stage: Stage 4
- Status: Finished
- ECMAScript edition: ES2027
- Synchronized: Aug 28, 2026
- 中文译文 · Source repository
This proposal introduces using and await using declarations for explicit, block-scoped resource management in ECMAScript, along with Symbol.dispose, Symbol.asyncDispose, and the DisposableStack/AsyncDisposableStack containers. It aims to standardize cleanup patterns for resources like file handles and streams.
The README below comes from the upstream repository and may contain outdated stage or status metadata. Use the proposal details above as the current source of truth.
ECMAScript Explicit Resource Management
NOTE: This proposal has subsumed the Async Explicit Resource Management proposal. This proposal repository should be used for further discussion of both sync and async of explicit resource management.
This proposal intends to address a common pattern in software development regarding the lifetime and management of various resources (memory, I/O, etc.). This pattern generally includes the allocation of a resource and the ability to explicitly release critical resources.
For example, ECMAScript Generator Functions and Async Generator Functions expose this pattern through the
return method, as a means to explicitly evaluate finally blocks to ensure
user-defined cleanup logic is preserved:
As such, we propose the adoption of a novel syntax to simplify this common pattern:
In addition, we propose the addition of two disposable container objects to assist with managing multiple resources:
DisposableStack— A stack-based container of disposable resources.AsyncDisposableStack— A stack-based container of asynchronously disposable resources.
Status
Stage: 3
Champion: Ron Buckton (@rbuckton)
Last Presented: March, 2023 (slides,
notes #1,
notes #2)
For more information see the TC39 proposal process.
Authors
- Ron Buckton (@rbuckton)
Motivations
This proposal is motivated by a number of cases:
-
Inconsistent patterns for resource management:
- ECMAScript Iterators:
iterator.return() - WHATWG Stream Readers:
reader.releaseLock() - NodeJS FileHandles:
handle.close() - Emscripten C++ objects handles:
Module._free(ptr) obj.delete() Module.destroy(obj)
- ECMAScript Iterators:
-
Avoiding common footguns when managing resources:
-
Scoping resources:
-
Avoiding common footguns when managing multiple resources:
-
Avoiding lengthy code when managing multiple resources correctly:
Compared to:
Compared to:
-
Non-blocking memory/IO applications:
-
Potential for use with the Fixed Layout Objects Proposal and
shared struct:
Prior Art
- C#:
- Java:
try-with-resources statement - Python:
withstatement
Definitions
- Resource — An object with a specific lifetime, at the end of which either a lifetime-sensitive operation should be performed or a non-garbage-collected reference (such as a file handle, socket, etc.) should be closed or freed.
- Resource Management — A process whereby "resources" are released, triggering any lifetime-sensitive operations or freeing any related non-garbage-collected references.
- Implicit Resource Management — Indicates a system whereby the lifetime of a "resource" is managed implicitly
by the runtime as part of garbage collection, such as:
WeakMapkeysWeakSetvaluesWeakRefvaluesFinalizationRegistryentries
- Explicit Resource Management — Indicates a system whereby the lifetime of a "resource" is managed explicitly
by the user either imperatively (by directly calling a method like
Symbol.dispose) or declaratively (through a block-scoped declaration likeusing).
Syntax
using Declarations
Grammar
Please refer to the specification text for the most recent version of the grammar.
await using Declarations
An await using declaration can appear in the following contexts:
- The top level of a Module anywhere VariableStatement is allowed, as long as it is not immediately nested inside of a CaseClause or DefaultClause.
- In the body of an async function or async generator anywhere a VariableStatement is allowed, as long as it is not immediately nested inside of a CaseClause or DefaultClause.
- In the head of a
for-oforfor-await-ofstatement.
await using in for-of and for-await-of Statements
You can use an await using declaration in a for-of or for-await-of statement inside of an async context to
explicitly bind each iterated value as an async disposable resource. for-await-of does not implicitly make a non-async
using declaration into an async await using declaration, as the await markers in for-await-of and await using
are explicit indicators for distinct cases: for await only indicates async iteration, while await using only
indicates async disposal. For example:
While there is some overlap in that the last three cases introduce some form of implicit await during execution, it
is intended that the presence or absence of the await modifier in a using declaration is an explicit indicator as to
whether we are expecting the iterated value to have an @@asyncDispose method. This distinction is in line with the
behavior of for-of and for-await-of:
using and await using have the same distinction:
This results in a matrix of behaviors based on the presence of each await marker:
Or, in table form:
Semantics
using Declarations
using Declarations with Explicit Local Bindings
When a using declaration is parsed with BindingIdentifier Initializer, the bindings created in the declaration
are tracked for disposal at the end of the containing Block or Module (a using declaration cannot be used
at the top level of a Script):
The above example has similar runtime semantics as the following transposed representation:
If exceptions are thrown both in the block following the using declaration and in the call to
[Symbol.dispose](), all exceptions are reported.
using Declarations with Multiple Resources
A using declaration can mix multiple explicit bindings in the same declaration:
These bindings are again used to perform resource disposal when the Block or Module exits, however in this case
[Symbol.dispose]() is invoked in the reverse order of their declaration. This is approximately equivalent to the
following:
Both of the above cases would have similar runtime semantics as the following transposed representation:
Since we must always ensure that we properly release resources, we must ensure that any abrupt completion that might occur during binding initialization results in evaluation of the cleanup step. When there are multiple declarations in the list, we track each resource in the order they are declared. As a result, we must release these resources in reverse order.
using Declarations and null or undefined Values
This proposal has opted to ignore null and undefined values provided to the using declarations. This is similar to
the behavior of using in C#, which also allows null. One primary reason for this behavior is to simplify a common
case where a resource might be optional, without requiring duplication of work or needless allocations:
Compared to:
using Declarations and Values Without [Symbol.dispose]
If a resource does not have a callable [Symbol.dispose] member, a TypeError would be thrown immediately when the
resource is tracked.
using Declarations in for-of and for-await-of Loops
A using declaration may occur in the ForDeclaration of a for-of or for-await-of loop:
In this case, the value bound to x in each iteration will be synchronously disposed at the end of each iteration.
This will not dispose resources that are not iterated, such as if iteration is terminated early due to return,
break, or throw.
using declarations may not be used in in the head of a for-in loop.
await using Declarations
await using Declarations with Explicit Local Bindings
When an await using declaration is parsed with BindingIdentifier Initializer, the bindings created in the
declaration are tracked for disposal at the end of the containing async function body, Block, or Module:
The above example has similar runtime semantics as the following transposed representation:
If exceptions are thrown both in the statements following the await using declaration and in the call to
[Symbol.asyncDispose](), all exceptions are reported.
await using Declarations with Multiple Resources
An await using declaration can mix multiple explicit bindings in the same declaration:
These bindings are again used to perform resource disposal when the Block or Module exits, however in this case each
resource's [Symbol.asyncDispose]() is invoked in the reverse order of their declaration. This is approximately
equivalent to the following:
Both of the above cases would have similar runtime semantics as the following transposed representation:
Since we must always ensure that we properly release resources, we must ensure that any abrupt completion that might occur during binding initialization results in evaluation of the cleanup step. When there are multiple declarations in the list, we track each resource in the order they are declared. As a result, we must release these resources in reverse order.
await using Declarations and null or undefined Values
This proposal has opted to ignore null and undefined values provided to await using declarations. This is
consistent with the proposed behavior for the using declarations in this proposal. Like in the sync case, this allows
simplifying a common case where a resource might be optional, without requiring duplication of work or needless
allocations:
Compared to:
await using Declarations and Values Without [Symbol.asyncDispose] or [Symbol.dispose]
If a resource does not have a callable [Symbol.asyncDispose] or [Symbol.asyncDispose] member, a TypeError would be thrown immediately when the resource is tracked.
await using Declarations in for-of and for-await-of Loops
An await using declaration may occur in the ForDeclaration of a for-await-of loop:
In this case, the value bound to x in each iteration will be asynchronously disposed at the end of each iteration.
This will not dispose resources that are not iterated, such as if iteration is terminated early due to return,
break, or throw.
await using declarations may not be used in in the head of a for-of or for-in loop.
Implicit Async Interleaving Points ("implicit await")
The await using syntax introduces an implicit async interleaving point (i.e., an implicit await) whenever control
flow exits an async function body, Block, or Module containing an await using declaration. This means that two
statements that currently execute in the same microtask, such as:
will instead execute in different microtasks if an await using declaration is introduced:
It is important that such an implicit interleaving point be adequately indicated within the syntax. We believe that
the presence of await using within such a block is an adequate indicator, since it should be fairly easy to recognize
a Block containing an await using statement in well-formatted code.
It is also feasible for editors to use features such as syntax highlighting, editor decorations, and inlay hints to further highlight such transitions, without needing to specify additional syntax.
Further discussion around the await using syntax and how it pertains to implicit async interleaving points can be
found in #1.
Examples
The following show examples of using this proposal with various APIs, assuming those APIs adopted this proposal.
WHATWG Streams API
NodeJS FileHandle
NodeJS Streams
Logging and tracing
Async Coordination
Three-Phase Commit Transactions
Shared Structs
main_thread.js
worker1.js
worker2.js
API
Additions to Symbol
This proposal adds the dispose and asyncDispose properties to the Symbol constructor, whose values are the
@@dispose and @@asyncDispose internal symbols:
Well-known Symbols
TypeScript Definition
The SuppressedError Error
If an exception occurs during resource disposal, it is possible that it might suppress an existing exception thrown
from the body, or from the disposal of another resource. Languages like Java allow you to access a suppressed exception
via a getSuppressed() method on
the exception. However, ECMAScript allows you to throw any value, not just Error, so there is no convenient place to
attach a suppressed exception. To better surface these suppressed exceptions and support both logging and error
recovery, this proposal seeks to introduce a new SuppressedError built-in Error subclass which would contain both
the error that was most recently thrown, as well as the error that was suppressed:
We've chosen to use SuppressedError over AggregateError 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 set of errors (i.e.,e.suppressed.suppressed.suppressedif there were successive error suppressions).- The only error correlation on
AggregateErroris throughcause, however aSuppressedErrorisn't "caused" by the error it suppresses. In addition,causeis intended to be optional, while theerrorof aSuppressedErrormust always be defined.
Built-in Disposables
%IteratorPrototype%.@@dispose()
We also propose to add Symbol.dispose to the built-in %IteratorPrototype% as if it had the following behavior:
%AsyncIteratorPrototype%.@@asyncDispose()
We propose to add Symbol.asyncDispose to the built-in %AsyncIteratorPrototype% as if it had the following behavior:
Other Possibilities
We could also consider adding Symbol.dispose to such objects as the return value from Proxy.revocable(), but that
is currently out of scope for the current proposal.
The Common Disposable and AsyncDisposable Interfaces
The Disposable Interface
An object is disposable if it conforms to the following interface:
TypeScript Definition
The AsyncDisposable Interface
An object is async disposable if it conforms to the following interface:
TypeScript Definition
The DisposableStack and AsyncDisposableStack container objects
This proposal adds two global objects that can act as containers to aggregate disposables, guaranteeing that every
disposable resource in the container is disposed when the respective disposal method is called. If any disposable in the
container throws an error during dispose, it would be thrown at the end (possibly wrapped in a SuppressedError if
multiple errors were thrown):
AsyncDisposableStack is the async version of DisposableStack and is a container used to aggregate async disposables,
guaranteeing that every disposable resource in the container is disposed when the respective disposal method is called.
If any disposable in the container throws an error during dispose, or results in a rejected Promise, it would be
thrown at the end (possibly wrapped in a SuppressedError if multiple errors were thrown):
These classes provided the following capabilities:
- Aggregation
- Interoperation and customization
- Assist in complex construction
NOTE:
DisposableStackis inspired by Python'sExitStack.
NOTE:
AsyncDisposableStackis inspired by Python'sAsyncExitStack.
Aggregation
The DisposableStack and AsyncDisposableStack classes provid the ability to aggregate multiple disposable resources
into a single container. When the DisposableStack container is disposed, each object in the container is also
guaranteed to be disposed (barring early termination of the program). If any resource throws an error during dispose,
it will be collected and rethrown after all resources are disposed. If there were multiple errors, they will be wrapped
in nested SuppressedError objects.
For example:
If all of resource1, resource2 and resource3 were to throw during disposal, this would produce an exception
similar to the following:
Interoperation and Customization
The DisposableStack and AsyncDisposableStack classes also provide the ability to create a disposable resource from a
simple callback. This callback will be executed when the stack's disposal method is executed.
The ability to create a disposable resource from a callback has several benefits:
- It allows developers to leverage
using/await usingwhile working with existing resources that do not conform to theSymbol.dispose/Symbol.asyncDisposemechanic: - It grants user the ability to schedule other cleanup work to evaluate at the end of the block similar to Go's
deferstatement:
Assist in Complex Construction
A user-defined disposable class might need to allocate and track multiple nested resources that should be disposed when
the class instance is disposed. However, properly managing the lifetime of these nested resources in the class
constructor can sometimes be difficult. The move method of DisposableStack/AsyncDisposableStack helps to more
easily manage lifetime in these scenarios:
Subclassing Disposable Classes
You can also use a DisposableStack to assist with disposal in a subclass constructor whose superclass is disposable:
Here, we can use stack to track the result of super() (i.e., the this value). If any exception occurs during
subclass construction, we can ensure that [Symbol.dispose]() is called, freeing resources. If the subclass also needs
to track its own disposable resources, this example is modified slightly:
In this example, we can simply add new resources to the stack and move its contents into the subclass instance's
this.#disposables. In the subclass [Symbol.dispose]() method we don't need to call super[Symbol.dispose]() since
that has already been tracked by the stack.defer call in the constructor.
Relation to Iterator and for..of
Iterators in ECMAScript also employ a "cleanup" step by way of supplying a return method. This means that there is
some similarity between a using declaration and a for..of statement:
However there are a number drawbacks to using for..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, StackOverflow answers, etc. - A
for..ofimplementation like the one above cannot control the scope ofuse, which can make lifetimes confusing: - 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 zero elements: - Using
continueandbreakis more difficult if you need to dispose of an iterated value:
Relation to DOM APIs
This proposal does not necessarily require immediate support in the HTML DOM specification, as existing APIs can still
be adapted by using DisposableStack or AsyncDisposableStack. However, there are a number of APIs that could benefit
from this proposal and should be considered by the relevant standards bodies. The following is by no means a complete
list, and primarily offers suggestions for consideration. The actual implementation is at the discretion of the relevant
standards bodies.
AudioContext—@@asyncDispose()as an alias or wrapper forclose().- NOTE:
close()here is asynchronous, but uses the same name as similar synchronous methods on other objects.
- NOTE:
BroadcastChannel—@@dispose()as an alias or wrapper forclose().EventSource—@@dispose()as an alias or wrapper forclose().FileReader—@@dispose()as an alias or wrapper forabort().IDbTransaction—@@dispose()could invokeabort()if the transaction is still in the active state:ImageBitmap—@@dispose()as an alias or wrapper forclose().IntersectionObserver—@@dispose()as an alias or wrapper fordisconnect().MediaKeySession—@@asyncDispose()as an alias or wrapper forclose().- NOTE:
close()here is asynchronous, but uses the same name as similar synchronous methods on other objects.
- NOTE:
MessagePort—@@dispose()as an alias or wrapper forclose().MutationObserver—@@dispose()as an alias or wrapper 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 or wrapper fordisconnect().PushSubscription—@@asyncDispose()as an alias or wrapper forunsubscribe().ReadableStream—@@asyncDispose()as an alias or wrapper forcancel().ReadableStreamDefaultReader— Either@@dispose()as an alias or wrapper forreleaseLock(), or@@asyncDispose()as a wrapper forcancel()(but probably not both).RTCPeerConnection—@@dispose()as an alias or wrapper forclose().RTCRtpTransceiver—@@dispose()as an alias or wrapper forstop().ReadableStreamDefaultController—@@dispose()as an alias or wrapper forclose().ReadableStreamDefaultReader— Either@@dispose()as an alias or wrapper forreleaseLock(), orResizeObserver—@@dispose()as an alias or wrapper fordisconnect().ServiceWorkerRegistration—@@asyncDispose()as a wrapper forunregister().SourceBuffer—@@dispose()as a wrapper forabort().TransformStreamDefaultController—@@dispose()as an alias or wrapper forterminate().WebSocket—@@dispose()as a wrapper forclose().Worker—@@dispose()as an alias or wrapper forterminate().WritableStream—@@asyncDispose()as an alias or wrapper 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 or wrapper forreleaseLock(), or@@asyncDispose()as a wrapper forclose()(but probably not both).XMLHttpRequest—@@dispose()as an alias or wrapper forabort().
In addition, several new APIs could be considered that leverage this functionality:
EventTarget.prototype.addEventListener(type, listener, { subscription: true }) -> Disposable— An option passed toaddEventListenercould return aDisposablethat removes the event listener when disposed.Performance.prototype.measureBlock(measureName, options) -> Disposable— Combinesmarkandmeasureinto a block-scoped disposable:SVGSVGElement— A new method producing a single-use disposer forpauseAnimations()andunpauseAnimations().ScreenOrientation— A new method producing a single-use disposer forlock()andunlock().
Definitions
A wrapper for x() is a method that invokes x(), but only if the object is in a state
such that calling x() will not throw as a result of repeated evaluation.
A callback-adapting wrapper is a wrapper that adapts a continuation passing-style method
that accepts a callback into a Promise-producing method.
A single-use disposer for x() and y() indicates a newly constructed disposable object
that invokes x() when constructed and y() when disposed the first time (and does nothing if the object is disposed
more than once).
Relation to NodeJS APIs
This proposal does not necessarily require immediate support in NodeJS, as existing APIs can still be adapted by using
DisposableStack or AsyncDisposableStack. However, there are a number of APIs that could benefit from this proposal
and should be considered by the NodeJS maintainers. The following is by no means a complete list, and primarily offers
suggestions for consideration. The actual implementation is at the discretion of the NodeJS maintainers.
- Anything with
ref()andunref()methods — A new method or API that produces a single-use disposer forref()andunref(). - Anything with
cork()anduncork()methods — A new method or API that produces a single-use disposer forcork()anduncork(). async_hooks.AsyncHook— either@@dispose()as an alias or wrapper fordisable(), or a new method that produces a single-use disposer forenable()anddisable().child_process.ChildProcess—@@dispose()as an alias or wrapper forkill().cluster.Worker—@@dispose()as an alias or wrapper forkill().crypto.Cipher,crypto.Decipher—@@dispose()as a wrapper forfinal().crypto.Hash,crypto.Hmac—@@dispose()as a wrapper fordigest().dns.Resolver,dnsPromises.Resolver—@@dispose()as an alias or wrapper forcancel().domain.Domain— A new method or API that produces a single-use disposer forenter()andexit().events.EventEmitter— A new method or API that produces a single-use disposer foron()andoff().fs.promises.FileHandle—@@asyncDispose()as an alias or wrapper forclose().fs.Dir—@@asyncDispose()as an alias or wrapper forclose(),@@dispose()as an alias or wrapper forcloseSync().fs.FSWatcher—@@dispose()as an alias or wrapper forclose().http.Agent—@@dispose()as an alias or wrapper fordestroy().http.ClientRequest— Either@@dispose()or@@asyncDispose()as an alias or wrapper fordestroy().http.Server—@@asyncDispose()as a callback-adapting wrapper forclose().http.ServerResponse—@@asyncDispose()as a callback-adapting wrapper forend().http.IncomingMessage— Either@@dispose()or@@asyncDispose()as an alias or wrapper fordestroy().http.OutgoingMessage— Either@@dispose()or@@asyncDispose()as an alias or wrapper fordestroy().http2.Http2Session—@@asyncDispose()as a callback-adapting wrapper forclose().http2.Http2Stream—@@asyncDispose()as a callback-adapting wrapper forclose().http2.Http2Server—@@asyncDispose()as a callback-adapting wrapper forclose().http2.Http2SecureServer—@@asyncDispose()as a callback-adapting wrapper forclose().http2.Http2ServerRequest— Either@@dispose()or@@asyncDispose()as an alias or wrapper fordestroy().http2.Http2ServerResponse—@@asyncDispose()as a callback-adapting wrapper forend().https.Server—@@asyncDispose()as a callback-adapting wrapper forclose().inspector— A new API that produces a single-use disposer foropen()andclose().stream.Writable— Either@@dispose()or@@asyncDispose()as an alias or wrapper fordestroy()or@@asyncDisposeonly as a callback-adapting wrapper forend()(depending on whether the disposal behavior should be to drop immediately or to flush any pending writes).stream.Readable— Either@@dispose()or@@asyncDispose()as an alias or wrapper fordestroy().- ... and many others in
net,readline,tls,udp, andworker_threads.
Meeting Notes
- 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 independent proposal.
- 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
TODO
The following is a high-level list of tasks to progress through each stage of the TC39 proposal process:
Stage 1 Entrance Criteria
- Identified a "champion" who will advance the addition.
- Prose outlining the problem or need and the general shape of a solution.
- Illustrative examples of usage.
- High-level API.
Stage 2 Entrance Criteria
- Initial specification text.
- Transpiler support (Optional).
Stage 3 Entrance Criteria
- Complete specification text.
- Designated reviewers have signed off on the current spec text:
- The ECMAScript editor has signed off on the current spec text.
Stage 4 Entrance Criteria
- Test262 acceptance tests have been written for mainline usage scenarios and merged.
- Two compatible implementations which pass the acceptance tests: [1], [2].
- A pull request has been sent to tc39/ecma262 with the integrated spec text.
- The ECMAScript editor has signed off on the pull request.
Implementations
- Built-ins from this proposal are available in
core-js