JavaScript `using`: Safer Cleanup for Production Apps


JavaScript resource management has historically depended on convention: open a file, remember to close it; start a stream, remember to cancel it; acquire a lock, remember to release it. That approach works until an early return, a thrown error, or a new branch quietly skips cleanup. Explicit resource management in JavaScript gives teams a language-level way to make cleanup part of the control flow instead of a hopeful code-review rule.

TL;DR: The using and await using declarations make resource lifetimes explicit. They are most useful for resources that must be released, closed, rolled back, or disconnected on every path through a scope. Adopt them behind a small adapter layer, test disposal order, and keep try/finally for environments that do not yet support the syntax.

Table of contents

The real problem is not memory

Garbage collection solves one narrow problem: reclaiming JavaScript memory once an object is no longer reachable. It does not tell an operating system to close a file descriptor at a predictable point. It cannot reliably terminate a database session, revert a temporary configuration, return a pooled connection, or remove a listener while the program is still running.

That is why production JavaScript is full of this shape:

js
const connection = await pool.connect();

try {
  const result = await connection.query('SELECT * FROM invoices');
  return result.rows;
} finally {
  connection.release();
}

There is nothing wrong with try/finally. It is one of the most dependable tools in the language. The problem emerges when cleanup is repeated across dozens of call sites, when a resource is passed through several helpers, or when an otherwise harmless refactor adds a path before the finally block. The resource and the cleanup rule become separated.

The same pattern appears in frontend code. A component may subscribe to an event source, create an object URL, start an animation, or reserve a Web Lock. Framework lifecycle hooks help, but they do not cover every utility function and every async workflow. The common failure is not that engineers do not know cleanup matters. It is that the ownership boundary is unclear.

Explicit resource management makes that boundary visible in the declaration itself.

What explicit resource management adds

The proposal introduces two declarations:

  • using for a resource with synchronous disposal
  • await using for a resource whose disposal is asynchronous

A value declared with using must provide a disposal method keyed by Symbol.dispose. A value declared with await using can provide Symbol.asyncDispose (and can also work with an appropriate synchronous disposer). When execution leaves the enclosing scope, JavaScript invokes the disposer automatically.

In simplified form, this:

js
{
  using log = openAuditLog();
  log.write('invoice exported');
}

behaves like this:

js
{
  const log = openAuditLog();
  try {
    log.write('invoice exported');
  } finally {
    log[Symbol.dispose]();
  }
}

The first version is not magic. Its value is that the acquisition and ownership model are co-located. A reader can see that log is scope-bound and should not be casually retained or returned.

This is particularly useful in libraries. Instead of documenting “call close() exactly once,” a library can expose a disposable object and let the caller express intent with using. The language takes responsibility for the boring, crucial path coverage.

Synchronous cleanup with using

Here is a small disposable timer that demonstrates the contract:

js
function startHeartbeat(onTick, intervalMs) {
  const id = setInterval(onTick, intervalMs);

  return {
    [Symbol.dispose]() {
      clearInterval(id);
    }
  };
}

function previewImport() {
  using heartbeat = startHeartbeat(() => console.log('working'), 1_000);

  // Parse and validate a local import.
  // The interval is cleared whether this succeeds, returns early, or throws.
  return validateImport();
}

A more realistic server example might wrap an observability span or a transaction-like unit of work:

js
class ScopedMetric {
  constructor(name, metrics) {
    this.name = name;
    this.metrics = metrics;
    this.startedAt = performance.now();
  }

  [Symbol.dispose]() {
    this.metrics.timing(this.name, performance.now() - this.startedAt);
  }
}

function renderDashboard(metrics) {
  using timing = new ScopedMetric('dashboard.render', metrics);
  return buildDashboardView();
}

The disposer should be focused and idempotent where practical. A disposal method is infrastructure code: it should not surprise callers by doing unrelated work, and it needs a clear policy for errors. For a simple local resource, clearing, closing, or releasing is usually enough.

Asynchronous cleanup with await using

Some cleanup is necessarily asynchronous. A stream may need to flush, a temporary remote resource may need deletion, or a database connection must be returned through an async pool API. This is where await using matters.

js
function acquirePreviewEnvironment(api) {
  return {
    async deploy() {
      return api.createPreview();
    },
    async [Symbol.asyncDispose]() {
      await api.deletePreview();
    }
  };
}

async function testPreview(api) {
  await using environment = acquirePreviewEnvironment(api);

  const url = await environment.deploy();
  return runSmokeTests(url);
}

When testPreview leaves its scope, JavaScript awaits the asynchronous disposer before resolving or rejecting the function. That gives the cleanup phase the same reliability as the main workflow.

Avoid using this feature simply because a function is async. await using communicates ownership, not just timing. If a shared client lives for the lifetime of an application, construct it at application startup and close it during graceful shutdown. Do not wrap every query in a freshly acquired client merely to use the new syntax.

How errors and disposal order work

Two rules are worth learning before adoption.

First, disposal happens when control exits a scope, including through return and exceptions. That is the core benefit.

Second, several resources are disposed in reverse declaration order, similar to nested finally blocks. This lets later resources depend on earlier ones during normal execution and be released first:

js
function generateReport() {
  using file = openReportFile();
  using writer = createCsvWriter(file);

  writer.writeRow(['customer', 'total']);
  // writer is disposed before file
}

If the body and a disposer both fail, JavaScript preserves information about both failures via SuppressedError. That is correct but unfamiliar to many application teams. Treat cleanup failures as real operational signals, especially around transactions and uploads. Your error reporting should capture the complete error chain rather than logging only a generic message.

Keep disposal small, observable, and tested. A test should cover success, an early return, a thrown error, and multiple resources. The happy path alone cannot validate why this feature exists.

A production adoption plan

Explicit resource management is a better tool, not a reason to perform a wholesale rewrite. A calm migration is usually safer.

1. Inventory the existing finally blocks

Search for patterns such as release(), close(), destroy(), abort(), unsubscribe(), and revokeObjectURL(). Prioritise code that manages scarce resources or production incidents have already touched: connection pools, streams, temporary files, locks, and long-running subscriptions.

2. Create narrow adapters

Most existing APIs will not immediately expose Symbol.dispose or Symbol.asyncDispose. Wrap them instead of modifying third-party objects:

js
function withConnection(pool) {
  return {
    async query(sql, params) {
      if (!this.connection) this.connection = await pool.connect();
      return this.connection.query(sql, params);
    },
    async [Symbol.asyncDispose]() {
      if (this.connection) this.connection.release();
    }
  };
}

async function loadInvoices(pool) {
  await using db = withConnection(pool);
  return db.query('SELECT id, total FROM invoices');
}

In real code, make the wrapper’s lifecycle explicit and avoid exposing the raw resource unless necessary. That prevents callers from using it after scope exit.

3. Verify runtime and build support

Syntax support matters in three places: the JavaScript runtime, the TypeScript compiler or transpiler, and the deployment target. Check the current compatibility guidance from MDN and test the exact Node.js and browser versions you ship. If some targets need transpilation, inspect the generated cleanup code and include it in your performance budget.

For libraries, do not force consumers into a syntax decision accidentally. Publish a clear minimum runtime version, or keep a compatible try/finally path until your supported environment has caught up.

4. Set ownership rules in code review

A useful team rule is simple: the code that acquires a short-lived resource owns its disposal, unless ownership is deliberately transferred. using makes this easy to enforce. Returning a using variable, caching it globally, or handing it to an unbounded background task should prompt a review comment.

5. Measure operational outcomes

Track pool exhaustion, open handles during tests, failed cleanup, and aborted request counts before and after migration. The win is not fewer lines of code. The win is fewer resources living longer than intended.

Where this fits in web applications

On the frontend, this pattern is most useful outside a framework’s normal component lifecycle:

  • Temporary object URLs created for previews
  • Event listeners and observers in imperative utilities
  • AbortController-backed workflows
  • Locks and exclusive editing sessions
  • Short-lived media or worker resources

In Node.js and edge workloads, it is especially compelling for:

  • Files, sockets, streams, and subprocess wrappers
  • Database or cache leases
  • Telemetry spans and tracing scopes
  • Temporary directories and generated assets
  • Test fixtures that must be torn down after failure

Framework cleanup remains important. React effects, server shutdown hooks, and request middleware describe longer lifetimes than a block scope. Use the scope that matches the resource. A subscription that should live while a component is mounted belongs in the component lifecycle; a helper’s temporary file belongs in using.

The broader design lesson is timeless: resource lifetimes should be expressed in code, not buried in a comment. JavaScript now has a more direct grammar for that idea. Teams that adopt it selectively can make the safest path the most readable one.

FAQ

Is using a replacement for try/finally?

No. It is a concise, structured form for the common case where a resource is owned by a lexical scope. Keep try/finally for compatibility, custom control flow, and cases where cleanup does not map naturally to a resource object.

What is the difference between using and await using?

using invokes a synchronous Symbol.dispose method. await using supports asynchronous disposal through Symbol.asyncDispose and waits for cleanup before leaving the scope.

Does garbage collection make this unnecessary?

No. Garbage collection manages memory, but it does not provide deterministic release of external resources such as connections, streams, files, or locks.

Should every service client use await using?

Usually not. Long-lived, shared clients should be managed by application startup and graceful-shutdown code. Use await using for short-lived resources whose ownership starts and ends within a specific operation.

What should teams test after adopting it?

Test normal completion, early returns, exceptions, disposal order, and disposal failures. Also verify your deployed runtime and build pipeline support the syntax you write.

Frequently Asked Questions

Is using a replacement for try/finally?

No. It is a structured option for resources owned by a lexical scope; try/finally remains valuable for compatibility and custom control flow.

What is the difference between using and await using?

using invokes synchronous disposal, while await using waits for asynchronous cleanup before scope exit.

Does garbage collection replace explicit resource cleanup?

No. Garbage collection reclaims memory, but it cannot deterministically release files, connections, streams, or locks.