
Stop Writing Manual Cleanup: The 'using' Keyword Is Now Your Garbage Collector
Stop cluttering your logic with fragile try...finally blocks and learn how to automate resource disposal with TypeScript's most underrated feature.
How many times have you written a perfect piece of logic, only to realize you’ve left a trail of open database connections, dangling file handles, or active network sockets in your wake?
We’ve all been there. You open a resource, you do some work, an error throws, and suddenly your close() or release() call is skipped entirely. To fix it, we resort to the "Try-Finally Nesting Doll"—a block of code so deeply indented it looks like a sideways mountain range. It’s ugly, it’s boilerplate-heavy, and frankly, we have better things to do.
TypeScript 5.2 quietly introduced a feature that solves this: Explicit Resource Management. With the using keyword, you can finally stop worrying about manual cleanup.
The "Before" Times: The Try-Finally Tax
Before we get to the good stuff, let's look at the mess we’re replacing. Imagine you’re working with a temporary file. You have to ensure that file is deleted regardless of whether your code succeeds or crashes.
import * as fs from 'fs';
function processData(path: string) {
const file = fs.openSync(path, 'w');
try {
// Do some complex logic here
fs.writeSync(file, 'Hello World');
if (Math.random() > 0.5) {
throw new Error("Something went sideways!");
}
} finally {
// This is the "babysitting" part
fs.closeSync(file);
console.log("Resource closed manually.");
}
}It works. But it’s brittle. If you have three different resources, you end up with three nested try...finally blocks or one giant mess at the bottom of the function. It’s a high mental tax for a low-level task.
The New Way: The using Keyword
The using keyword allows you to declare a variable that "disposes" itself as soon as it goes out of scope. No finally block required.
For a class or object to work with using, it just needs to implement a specific method: Symbol.dispose.
class TempFile implements Disposable {
constructor(public path: string) {
console.log(`Opening ${path}`);
}
[Symbol.dispose]() {
console.log(`Automatically cleaning up ${this.path}`);
// Logic to close or delete the file goes here
}
}
function doWork() {
using file = new TempFile("debug.log");
// Do whatever you want here
console.log("Doing work...");
if (true) return; // Even if we return early, the file is disposed!
}
doWork();When doWork finishes (or throws an error), TypeScript automatically calls the code inside [Symbol.dispose]. It feels like magic, but it’s actually just a very smart compiler transformation.
What about Async?
In the world of Node.js and modern web apps, our cleanup is rarely synchronous. You’re usually closing a database pool or ending a session, which involves a Promise.
TypeScript handles this with await using and Symbol.asyncDispose.
class DatabaseConnection implements AsyncDisposable {
async connect() {
console.log("Connected to DB");
}
async [Symbol.asyncDispose]() {
console.log("Closing connection gracefully...");
await new Promise(resolve => setTimeout(resolve, 500));
console.log("Disconnected.");
}
}
async function runQuery() {
await using db = new DatabaseConnection();
await db.connect();
console.log("Executing query...");
// Once this function block ends, the DB closes itself.
}This is a game-changer for server-side TypeScript. You no longer have to track which connections are active; if the variable goes out of scope, the connection dies. Period.
Why this is better than "Garbage Collection"
You might be thinking, "Isn't this what the Garbage Collector (GC) is for?"
Not exactly. The GC is great for memory, but it’s non-deterministic. You don't know *when* the GC will run. If you're holding onto a limited resource—like a file lock or a premium API connection—you can't wait for the GC to feel like waking up. You need that resource freed *now*.
using gives you the convenience of automatic management with the precision of manual cleanup.
The "Gotchas" (Because there are always some)
1. TypeScript Version: You need TS 5.2 or higher.
2. Polyfills: This relies on Symbol.dispose and Symbol.asyncDispose. If you’re targeting older environments (like older Node versions or IE—god forbid), you’ll need a polyfill for these symbols.
3. The `lib` Compiler Option: In your tsconfig.json, you need to include "esnext.disposable" in your lib array to let the compiler know about these new types.
{
"compilerOptions": {
"target": "ESNext",
"lib": ["esnext", "esnext.disposable"]
}
}Wrap up
The using keyword is one of those rare features that makes code both safer and shorter. It eliminates an entire category of "oops, I forgot to close that" bugs while making your functions significantly more readable.
I’ve started using it for test database setups and temporary logging buffers, and I haven't looked back. Next time you're about to wrap your logic in a try...finally, ask yourself if a Disposable class might be a cleaner way to live. Stop babysitting your variables—let TypeScript handle the trash.


