using Resource Management, WeakRef, Import Attributes
TL;DR
Three forward-looking features. using and await using (Stage 3, ES2026 likely) bring deterministic resource cleanup to JS — declare a disposable, it auto-disposes when scope exits. WeakRef + FinalizationRegistry (ES2021) give you weak references and “run a cleanup when this object is GC’d” — for caches, observers, and library plumbing. Import attributes (import x from 'y' with { type: 'json' }, ES2025) standardize per-import metadata, replacing the deprecated assert syntax.
using Q&A
Q: What’s using?
A: Stage 3 proposal — declarative resource disposal. A using declaration binds a value implementing Symbol.dispose; when the enclosing scope exits, JS calls [Symbol.dispose]() automatically.
// Resource that implements [Symbol.dispose]
class FileHandle {
constructor(public path: string) {
console.log(`open ${path}`);
}
[Symbol.dispose]() {
console.log(`close ${this.path}`);
}
}
function readFile(path: string) {
using fh = new FileHandle(path);
// ... use fh ...
} // [Symbol.dispose] called here automatically
readFile("foo.txt");
// open foo.txt
// close foo.txt
Same pattern with try/finally would be:
function readFile(path: string) {
const fh = new FileHandle(path);
try {
// ... use fh ...
} finally {
fh.dispose();
}
}
The using declaration encodes “dispose on scope exit” so you can’t forget. Multiple using declarations dispose in reverse declaration order (like nested try/finally).
Q: await using for async resources.
A: For resources whose disposal is async (close DB connection, await stream end):
class DbConnection {
async [Symbol.asyncDispose]() {
await this.close();
}
async close() { /* ... */ }
}
async function doWork() {
await using db = await DbConnection.connect();
// ... use db ...
} // await db[Symbol.asyncDispose]() called
await using awaits the disposal. Composes naturally — multiple await using resources dispose in reverse order.
Q: When use using over plain try/finally?
A:
- Multiple resources —
using a; using b; using c;is much cleaner than 3 nested try/finally. - Library APIs that ship with
Symbol.dispose(Prisma, Drizzle, file handles in Node 20+). - Test cleanup —
using server = await startTestServer();ensures tear-down even onexpectthrows.
When not:
- Existing codebases without the feature available (Node < 20, TS < 5.2 without target).
- One-off resources where try/finally is one line.
Q: Engine / tooling support.
A:
- TypeScript 5.2+ — full support behind
target: ES2022+. - Chrome 134+ / Node 24+ — native
usingsupport. - Babel / SWC — plugin available; transpiles to try/finally.
- Library uptake — Node fs handles, database clients adopting
Symbol.disposeas it stabilizes.
Until native, TS users get the syntax, transpiler emits try/finally.
Q: using + for-of?
A: using inside a loop disposes per iteration:
async function processAll(paths: string[]) {
for (const path of paths) {
using fh = new FileHandle(path); // disposed per iteration
// ...
}
}
For batched disposal, you’d hold an array of resources and dispose them all at the end (manually or via a managing class).
WeakRef Q&A
Q: What’s WeakRef?
A: A reference to an object that does not prevent garbage collection. Normal references keep the object alive; weak references don’t.
const obj = { name: "Ada", data: largeArray };
const weak = new WeakRef(obj);
// Clear the strong reference
// (in a real scenario, obj is set to null or goes out of scope)
setTimeout(() => {
const deref = weak.deref();
if (deref) {
console.log(deref.name); // still alive
} else {
console.log("GC'd"); // collected
}
}, 1000);
weak.deref() returns the object if it’s still alive, undefined if collected.
Q: Use cases?
A:
- Caches — keep a
WeakRefto a recently-used object; if it’s still around (referenced elsewhere), return it; if collected, fetch again. - Observers/subscribers — a parent holds weak refs to children; children can be GC’d without the parent leaking them.
- Library plumbing — frameworks tracking component instances without preventing teardown.
class WeakCache<K, V extends object> {
#map = new Map<K, WeakRef<V>>();
get(key: K): V | undefined {
const ref = this.#map.get(key);
return ref?.deref(); // may be undefined if GC'd
}
set(key: K, value: V) {
this.#map.set(key, new WeakRef(value));
}
}
For most app code, avoid WeakRef — it makes behavior depend on GC timing, which is non-deterministic. Reserve for library code.
Q: WeakMap and WeakSet — different from WeakRef?
A: Yes:
WeakMap<K, V>— keys are weakly held. If the key object is GC’d, the entry vanishes. Values are normally held.WeakSet<T>— items are weakly held.WeakRef<T>— single object reference, weakly held.
Use cases:
WeakMapfor “metadata about objects” without preventing their cleanup (private class data, observer state).WeakSetfor “objects I’ve seen / processed” without retaining them.WeakReffor explicit “may or may not still be alive.”
Q: FinalizationRegistry — what?
A: Run a callback when an object is GC’d:
const registry = new FinalizationRegistry<string>((heldValue) => {
console.log(`object with ${heldValue} was collected`);
});
let obj = { name: "Ada" };
registry.register(obj, "Ada's data");
obj = null; // make it eligible for GC
// Eventually (when GC runs), the callback fires
Useful for:
- Closing external resources (file handles, native bindings) the JS object wrapped.
- Cache eviction tied to GC.
Important caveat: GC timing is non-deterministic. The callback may fire seconds later, or not at all (if the engine doesn’t need to GC). Never rely on it for correctness — only for cleanup that’s safe to defer or skip.
Q: When not to use WeakRef / FinalizationRegistry?
A:
- App business logic — non-determinism is a bug source.
- Critical cleanup — use
Symbol.dispose/usinginstead for deterministic teardown. - Anything you can test for — the GC may not run in your tests, hiding bugs.
The MDN warning is direct: “Avoid where possible.” These are advanced library tools, not general-purpose features.
Import Attributes Q&A
Q: What’s the with syntax?
A: ES2025 — metadata on imports, primarily for non-JS resources:
import data from "./config.json" with { type: "json" };
import wasm from "./module.wasm" with { type: "webassembly" };
import css from "./styles.css" with { type: "css" };
// Dynamic
const mod = await import("./data.json", { with: { type: "json" } });
Required by Node 22+ for JSON imports; bundlers (Vite, Webpack) accept and pre-process.
Q: Why not the old assert syntax?
A: TC39 changed direction. assert { type: "json" } (Stage 3 for a while) was renamed to with because the semantics are “hints to the loader,” not “assertions to be checked.” The old syntax is deprecated; with is the standard.
Migration: tooling generally accepts both temporarily. New code uses with.
Q: What attributes exist?
A: Currently standardized: type (for "json", "webassembly", "css"). More may come (integrity for SRI, others).
The mechanism is generic — host environments (browsers, Node, bundlers) can support additional attributes.
Gotchas / edge cases
usingwithout target = ES2022 in TS — earlier targets transpile via a polyfill helper.Symbol.disposecollisions — your resource class must implement the symbol exactly; typos result in no-op cleanup.await usingin non-async function — error; must be insideasync function.WeakRef.deref()returnsundefinedwhen collected — always check before use.- GC may not run in tests —
WeakRef/FinalizationRegistrytests are flaky or skipped. - Import attributes browser support — Chrome 123+, Safari 17.2+, Firefox 130+. Polyfill via bundler.
- JSON imports without attribute — error in Node 22+; warning earlier.
What a senior is expected to say
- “
using(Stage 3) gives deterministic cleanup — declare a disposable, it auto-disposes when scope exits. Like Python’swith, C#’susing, Java’s try-with-resources.” - “
await usingfor async disposables (DB connections, streams). Disposes in reverse declaration order.” - “
WeakRefandFinalizationRegistryfor library plumbing — caches, observers. Avoid in app logic; GC timing is non-deterministic.” - “Import attributes (
with { type: 'json' }) replaced the deprecatedassert {}syntax. Required for JSON in modern Node.” - “Adoption strategy: use TS 5.2+ syntax with appropriate target; ship via bundler transpilation until native everywhere.”
Cross-references
- Class features (
Symbol.disposelives on classes): 02_class_features.md - Module formats (where import attributes apply): ../09_build_tools/03_module_formats.md
- Memory model (relates to WeakRef): ../05_react/ (for understanding lifetimes)
Further reading
- TC39 — Explicit Resource Management (
using): https://github.com/tc39/proposal-explicit-resource-management - MDN —
Symbol.dispose: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/dispose - MDN —
WeakRef: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef - MDN — Import attributes: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import/with