Oct 19, 2026
Your teardown logic is now a language feature
JavaScript's `using` declaration turns the cleanup code every dashboard developer writes by hand — aborting requests, closing sockets, clearing timers — into something the runtime guarantees.
Every dashboard I've built has a graveyard of cleanup code. controller.abort() in the effect return. removeEventListener calls I triple-checked. A WebSocket close() in the unmount path that I definitely remembered — most of the time.
The bug is never the feature code. It's the teardown that didn't run because an early return skipped it, an exception jumped over it, or someone added a third resource and forgot to extend the finally.
JavaScript just got a language-level answer: the using declaration.
The shape of the problem
Here's the pattern we all write:
function watchMetrics(symbol) {
const controller = new AbortController();
const ws = new WebSocket(url);
try {
// subscribe, render, update
} finally {
controller.abort();
ws.close();
}
}
Two resources, one finally. Add a third — an interval polling a fallback endpoint — and the finally grows. Worse: if acquiring the third resource throws, the first two still need releasing, which means nested try/finally blocks. MDN's own docs show this nesting getting ugly fast. The logic drowns in plumbing.
What using does
A using declaration ties cleanup to scope exit instead of control flow. The contract is one method — [Symbol.dispose]() — no base class, no framework:
function abortable() {
const controller = new AbortController();
return {
signal: controller.signal,
[Symbol.dispose]() { controller.abort(); },
};
}
{
using req = abortable();
const res = await fetch(url, { signal: req.signal });
const data = await res.json();
render(data);
} // controller.abort() runs here — normal exit, early return, or throw
Five lines of adapter, and it's the last time you write that cleanup by hand. Normal exit, early return, thrown exception — disposal runs either way.
The part that matters: DisposableStack
Real teardown is rarely one resource. DisposableStack collects several and releases them in reverse order — last acquired, first released:
function watchMetrics(symbol) {
using stack = new DisposableStack();
const req = stack.use(abortable());
const ws = stack.use(socketResource(url));
const timer = stack.adopt(setInterval(pollFallback, 5000), clearInterval);
// subscribe, render, update
} // timer cleared, socket closed, request aborted — LIFO, guaranteed
If acquiring the third resource throws, the first two still get cleaned up. No manual rollback logic. That's the exact case hand-written finally blocks get wrong most often.
Where this stands
TypeScript has supported using since 5.2. Node 22+ and current Chrome and Firefox run it natively. The ecosystem is still catching up — most libraries don't ship disposable handles yet, so you'll write small adapters like the ones above. That's fine.
The deeper point: cleanup was always a lifetime problem, and we were solving it with control-flow tools. try/finally answers "what happens when this block exits." using answers "what happens when this resource's life ends." Those sound similar until the third resource shows up.