Frontend / ECMAScript features / 08_using_and_weakref.md

using Resource Management, WeakRef, Import Attributes

Updated 4 interview angles 7 min read source
On this page7
  1. using
  2. WeakRef
  3. Import Attributes
  4. Gotchas
  5. Related
  6. Further reading
  7. Interview angle

using Resource Management, WeakRef, Import Attributes

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

What’s using?

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.

ts
// 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:

ts
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).

await using for async resources.

For resources whose disposal is async (close DB connection, await stream end):

ts
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.

When use using over plain try/finally?

  • 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 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.

Engine / tooling support.

  • 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.

using + for-of?

using inside a loop disposes per iteration:

ts
async function processAll(paths: string[]) {
  for (const path of paths) {
    // disposed per iteration
    using fh = new FileHandle(path);
    // ...
  }
}

For batched disposal, you’d hold an array of resources and dispose them all at the end (manually or via a managing class).

WeakRef

What’s WeakRef?

A reference to an object that does not prevent garbage collection. Normal references keep the object alive; weak references don’t.

ts
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.

Use cases?

  • 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.
ts
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.

WeakMap and WeakSet — different from WeakRef?

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.”

FinalizationRegistry — what?

Run a callback when an object is GC’d:

ts
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.

When not to use WeakRef / FinalizationRegistry?

  • 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

What’s the with syntax?

ES2025 — metadata on imports, primarily for non-JS resources:

ts
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.

Why not the old assert syntax?

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.

What attributes exist?

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

  • 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 tests — WeakRef/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.

Further reading

Interview angle 4

  • “What does using give you?” - deterministic cleanup scoped to a block. A disposable declared with using has its [Symbol.dispose] called when the scope exits, however it exits, which is the same guarantee as Python’s with or Java’s try-with-resources.
    • Follow-up: “What about something async?” - await using for [Symbol.asyncDispose], which is what a database connection or a stream needs. Disposal runs in reverse declaration order, so resources unwind in the order you would have written by hand.
  • “When would you use WeakRef or FinalizationRegistry?” - in library plumbing - caches and observer registries where holding a strong reference would leak. Keep them out of application logic: collection timing is not deterministic and finalizers are explicitly not guaranteed to run, so nothing correctness-critical can depend on one.
  • “How do you import JSON now?” - import attributes: with { type: "json" }. The assert { ... } form is deprecated, and the attribute is required in modern Node, so seeing assert in a codebase dates it.
  • “How do you ship syntax that is still Stage 3?” - write it against TypeScript 5.2+ with a target the bundler can lower, and let the build emit the downlevel form until native support is everywhere. The syntax is stable enough to adopt; the runtime support is what you are waiting on.