JavaScript Explicit Resource Management
Learn using, await using, Symbol.dispose, Symbol.asyncDispose, DisposableStack, AsyncDisposableStack, scope-based cleanup, ownership transfer, and deterministic resource lifetimes.
Advanced
Explicit Resource Management gives JavaScript a language-level way to associate cleanup with a scope. The primary tools are using and await using declarations, Symbol.dispose and Symbol.asyncDispose, DisposableStack, and AsyncDisposableStack. The important distinction is deterministic disposal at a defined scope boundary, not garbage-collection-based cleanup.
Why deterministic cleanup is different
Garbage collection answers a different question: when an unreachable object may be reclaimed. Explicit Resource Management answers: when a program has finished using a resource, which cleanup operation must run at the end of this scope? That makes resource lifetime explicit and predictable.
Output
Ready to run.
Quick check
Which problem is Explicit Resource Management designed to solve?
The disposal protocols
A synchronous disposable exposes a callable method at Symbol.dispose. An asynchronous disposable exposes a method at Symbol.asyncDispose that returns a Promise. These well-known Symbols are the protocol hooks used by using declarations and the disposable stacks.
Output
Ready to run.
Output
Ready to run.
Quick check
Which protocol belongs to synchronous disposal?
using declarations
A using declaration registers a resource for synchronous disposal when the immediately containing scope completes. Conceptually, the resource is acquired first and its disposal method is captured for the scope. Resources are disposed in reverse initialization order.
using resource = acquireResource();
use(resource);
// resource is disposed when this containing scope completes.
Output
Ready to run.
Quick check
When does a using resource get automatically disposed?
await using and asynchronous disposal
await using is the asynchronous form. Its resource uses the async-disposal protocol and scope completion includes awaiting asynchronous cleanup. This is particularly useful when releasing a resource itself requires asynchronous work.
await using connection = await openConnection();
await query(connection);
// async disposal is awaited as the containing scope completes.
Output
Ready to run.
Quick check
What distinguishes await using from using?
Nullish resources
The disposal model treats null and undefined specially. A nullish synchronous resource does not create a disposal operation. For asynchronous disposal, the language still preserves the required asynchronous scope-completion behavior.
Output
Ready to run.
DisposableStack
DisposableStack collects synchronous disposable resources and releases them together. Resources are stored in acquisition order and disposed in reverse order. It can also adopt arbitrary values with a custom cleanup function and defer a cleanup callback.
Output
Ready to run.
Output
Ready to run.
Quick check
In what order does a DisposableStack dispose resources?
use, adopt, and defer
DisposableStack.use(value) registers a disposable value. DisposableStack.adopt(value, onDispose) associates an arbitrary value with a cleanup callback. DisposableStack.defer(onDispose) registers cleanup that does not need a resource value. These methods are registration mechanisms; disposal still happens when the stack itself is disposed.
Output
Ready to run.
move transfers ownership
DisposableStack.move() transfers the pending resource stack to a new stack and marks the original stack disposed. This is an ownership transfer: after the move, the new stack owns the pending cleanup work.
Output
Ready to run.
AsyncDisposableStack
AsyncDisposableStack is the asynchronous counterpart. Its disposeAsync method performs asynchronous cleanup, and use, adopt, and defer register resources or cleanup callbacks for the stack. Its resource list is also unwound in reverse order.
Output
Ready to run.
Output
Ready to run.
Errors during disposal
Disposal is part of scope completion. If cleanup throws or rejects, that completion participates in the language error propagation rules. When multiple disposal operations are involved, the resource-management machinery preserves the relevant errors rather than silently treating cleanup as optional.
Output
Ready to run.
Quick check
Should disposal errors be ignored because cleanup happens after the main work?
Disposal and ordinary try/finally
try/finally remains a general-purpose control-flow mechanism and is not replaced by using. Explicit Resource Management gives resources a standard disposal protocol and scope-based unwinding. Do not duplicate cleanup logic merely to imitate using when the resource already implements the disposal protocol.
Output
Ready to run.
Resource ownership is the key design decision
A good disposable object owns the cleanup responsibility for the resource it represents. Callers acquire it, use it, and let the appropriate scope or stack release it. This avoids coupling every caller to low-level cleanup details.
Output
Ready to run.
Do not confuse disposal with garbage collection
Explicit disposal is deterministic program behavior. Garbage collection is an implementation-managed memory reclamation process. A disposable resource should not depend on WeakRef or FinalizationRegistry to perform required cleanup, and a finalizer should not be treated as a substitute for explicit disposal.
Output
Ready to run.
Scope and lifetime
The central mental model is: acquisition creates a responsibility, the scope records that responsibility, and scope completion unwinds it. Nested scopes therefore create nested resource lifetimes. A resource acquired in an inner scope is disposed before a resource owned by an outer scope.
Output
Ready to run.
Authoring rules
Use explicit resource management when a resource has a meaningful cleanup operation and its lifetime naturally matches a lexical scope. Make disposal idempotent where practical. Use synchronous disposal for synchronous cleanup and asynchronous disposal when cleanup must await asynchronous work. Keep required cleanup deterministic; never rely on garbage collection timing.
Quick check
Which design is appropriate for a file-like resource?
Feature detection and portability
Resource Management is part of modern ECMAScript, but deployed runtimes can lag the current specification. If code must run across mixed environments, detect the required syntax or built-ins and provide an appropriate compatibility strategy rather than assuming support.
Output
Ready to run.
Quick mental model
Remember four layers: protocol (Symbol.dispose / Symbol.asyncDispose), declaration (using / await using), aggregation (DisposableStack / AsyncDisposableStack), and lifetime (scope completion). Weak references belong to a different problem: observing liveness without strong ownership.
When cleanup itself fails
Disposal is part of resource-management completion. If a cleanup operation throws while the surrounding scope is already completing abruptly, JavaScript must preserve both failures. SuppressedError represents that relationship: its error property contains the newer cleanup failure, while suppressed contains the earlier failure that was already in flight.
Output
Ready to run.
Quick check
Which property of SuppressedError preserves the completion that was already being propagated?
SuppressedError is an Error object
SuppressedError is a standard Error object with its own constructor and prototype. Its error and suppressed properties are the two semantic payloads that distinguish it from ordinary Error objects. The properties are non-enumerable, so ordinary key enumeration does not expose them.
Output
Ready to run.
Quick check
Which statement is correct?
How disposal creates a SuppressedError
The important case is not merely constructing SuppressedError yourself. During resource disposal, if the scope is already propagating an abrupt completion and a later disposal operation also throws, the disposal algorithm creates a SuppressedError so that neither failure is silently discarded. If there was no earlier abrupt completion, the disposal failure can propagate directly.
Output
Ready to run.
Quick check
Why does disposal need SuppressedError when the scope already has an error?
Ordering: the later cleanup failure becomes error
When a disposal failure occurs while another abrupt completion is already active, the newly created SuppressedError places the disposal failure in error and the earlier completion in suppressed. This ordering matters when diagnosing nested cleanup failures: error answers “what failed while cleaning up?” and suppressed answers “what was already failing?”
Output
Ready to run.
Quick check
In a SuppressedError produced by a later disposal failure, which value belongs in error?
Do not confuse SuppressedError with AggregateError
AggregateError groups multiple errors as an errors collection. SuppressedError has a different purpose: it records a primary error produced during the current operation together with an earlier completion that became secondary because cleanup failed. The two structures should not be taught as interchangeable error containers.
Output
Ready to run.
Quick check
What distinguishes SuppressedError from AggregateError?
Authoring rule: preserve cleanup diagnostics
When designing disposable resources, let disposal errors propagate rather than swallowing them. SuppressedError exists so deterministic cleanup can remain observable without losing an already-active failure. Application code can then inspect error and suppressed, log the relationship, or add higher-level context without pretending the two failures were unrelated.
Quick check
What should a disposable resource normally do if its cleanup operation fails?
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.