Glossary / Async resource cleanup

Async resource cleanup

Async resource cleanup ensures that resources acquired by asynchronous work—files, sockets, locks, timers, subscriptions, readers, workers, or transactions—are released on success, failure, and cancellation. Because operations suspend and resume, cleanup must be tied to a clear owner and awaited when release itself is asynchronous.

Structured lifetime with try/finally

The basic pattern is to acquire, use, and release within one lexical scope. finally runs after normal completion, exceptions, or a return, and await inside it waits for asynchronous cleanup.

const reader = stream.getReader();
try {
  return await consume(reader);
} finally {
  reader.releaseLock();
}

For a socket, lock, or transaction, cleanup might be await close(), await rollback(), or clearTimeout(id). If cleanup can fail, decide whether that failure replaces the original error, is aggregated, or is logged while preserving the primary failure. Do not start cleanup and abandon its promise.

AbortController communicates a cancellation request, but does not itself release resources: each operation must observe the signal and perform cleanup. Promise.race with a timeout also does not cancel the losing operation. Resource ownership must outlive the work or explicitly transfer to another owner.

Explicit resource management

JavaScript's Explicit Resource Management feature adds using and await using declarations for objects implementing disposal protocols (Symbol.dispose and Symbol.asyncDispose). Availability depends on the target runtime and toolchain; verify support before adopting. These constructs provide scope-based disposal, including async disposal for await using, while traditional try/finally remains broadly compatible and explicit.

Engineering guidance

Model each resource's owner, acquisition point, transfer point, and cleanup action. Make cleanup idempotent where practical. Handle partial acquisition: if acquiring the second resource fails, release the first. Test failure at every await boundary, cancellation during pending I/O, repeated shutdown, and cleanup errors. For long-running services, support graceful shutdown with bounded drain time and observable forced termination.

Interview questions

  1. Does aborting a signal close a socket automatically? Only if the API observes the signal and implements that behavior.
  2. Why use finally? It runs across success, throw, and return paths, keeping cleanup attached to ownership scope.
  3. Does Promise.race cancel losing work? No; cancellation must be propagated explicitly.
  4. What if cleanup throws while handling another error? Define policy to preserve or aggregate the primary failure instead of silently masking it.
  5. What does await using require? Runtime support and an object implementing the async disposal protocol.

References