frontend / es features / 08_using_and_weakref.md

using Resource Management, WeakRef, Import Attributes

6 min read source

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 resourcesusing 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 cleanupusing server = await startTestServer(); ensures tear-down even on expect throws.

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 using support.
  • Babel / SWC — plugin available; transpiles to try/finally.
  • Library uptake — Node fs handles, database clients adopting Symbol.dispose as 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 WeakRef to 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:

  • WeakMap for “metadata about objects” without preventing their cleanup (private class data, observer state).
  • WeakSet for “objects I’ve seen / processed” without retaining them.
  • WeakRef for 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 / using instead 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

  • using without target = ES2022 in TS — earlier targets transpile via a polyfill helper.
  • Symbol.dispose collisions — your resource class must implement the symbol exactly; typos result in no-op cleanup.
  • await using in non-async function — error; must be inside async function.
  • WeakRef.deref() returns undefined when collected — always check before use.
  • GC may not run in testsWeakRef/FinalizationRegistry tests 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’s with, C#’s using, Java’s try-with-resources.”
  • await using for async disposables (DB connections, streams). Disposes in reverse declaration order.”
  • WeakRef and FinalizationRegistry for library plumbing — caches, observers. Avoid in app logic; GC timing is non-deterministic.”
  • “Import attributes (with { type: 'json' }) replaced the deprecated assert {} 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

Further reading