Skip to content
Foxipa

Suggest an edit

Tell us what should be corrected, clarified, or improved. Submission will be connected to Foxipa’s contribution service later.

No information is sent anywhere yet. Your suggestion is only prepared in your browser.

Learning

JavaScript Explicit Resource Management

Learn using, await using, Symbol.dispose, Symbol.asyncDispose, DisposableStack, AsyncDisposableStack, scope-based cleanup, ownership transfer, and deterministic resource lifetimes.

Levels: Advanced ~70 min read

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

Ready to run.

Interactive Runnable example
Edit

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.

javascript
using resource = acquireResource();
use(resource);
// resource is disposed when this containing scope completes.
Interactive Runnable example
Edit

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.

javascript
await using connection = await openConnection();
await query(connection);
// async disposal is awaited as the containing scope completes.
Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

Ready to run.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

Ready to run.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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?”

Interactive Runnable example
Edit

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.

Interactive Runnable example
Edit

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?

Was this page helpful?

Mark this learning resource complete to track your learning progress.