cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

WeakRef & FinalizationRegistry

Learn WeakRef, deref, FinalizationRegistry, weak caches, and why garbage-collection cleanup timing cannot be trusted in JavaScript.

By the end, you can
  • 01
    Read weak references safelyUse new WeakRef(target) and deref() while handling undefined every time.
  • 02
    Register finalization hintsConnect FinalizationRegistry, held values, unregister tokens, and the rule that holdings must not be the target.
  • 03
    Design honest weak cachesCombine Map keys, WeakRef values, and guarded registry cleanup without depending on callback timing.

Weak reachability, not weak responsibility

JavaScript normally works with strong references: if a variable, object property, closure, array, or map entry can reach an object, the garbage collector must keep that object. A weak reference is different. It lets your code point at a target without making that target reachable for the future.

Definition

WeakRef holds an object or non-registered symbol weakly. Its deref() method returns the target if it is still alive, or undefined if it has been collected. FinalizationRegistry can run a callback with a held value after a target is collected, but that callback is never guaranteed to run at a specific time, or at all.

Start by separating this from WeakMap and WeakSet. A WeakMap weakens its keys: the object key can disappear without the map keeping it alive. A WeakRef weakens a target you read through deref(), so it is closer to a weak value. Both ideas depend on the reachability rules from memory management.

Real-life analogyA photo of a friend

A photo lets you look at a friend without making them stay nearby. A WeakRef has the same shape: ask, check the answer, and move on.

In real life: A photo shows your friend
In JavaScript: A WeakRef points to a target
In real life: The photo does not keep them nearby
In JavaScript: The weak reference does not keep the target alive
In real life: You check whether they are around
In JavaScript: Call deref() and handle undefined

Where the analogy stops: A photo stays visible after someone leaves. A weak reference can later return undefined, but JavaScript does not promise when.

Four tools that are easy to confuse
ToolWhat is weak or deterministicGood first use
WeakMapWeak keys with ordinary valuesAttach metadata to an object key without keeping the key alive.
WeakRefWeak value-like referenceHold a target weakly and ask deref() whether it is still available.
FinalizationRegistryA possible cleanup notificationReceive a held value after a target is collected, if the engine runs the callback.
Explicit cleanupDeterministic code you callUse try/finally, .close(), .dispose(), or using for essential resources.

This lesson also connects back to closures, because closures can accidentally keep values alive, and symbols, because non-registered symbols can be weak targets. We will link forward to resource management for deterministic cleanup patterns such as using.

WeakRef and deref()

STEP THROUGH

Creating a weak reference is direct: new WeakRef(target). Reading it is deliberately indirect: call ref.deref(), then branch on the result. If the result is an object, use it immediately. If it is undefined, rebuild the value or treat the cache as a miss.

The first experiment shows a subtle guarantee from the specification. After a target is created as a WeakRef target or returned by deref(), it is kept alive until the end of the current synchronous job. That is sometimes called KeepDuringJob. It does not make the object permanent.

WeakRef job lab: what does deref return?
Step 0 of 7Ready
Your turn: follow the blue line

Step through a WeakRef read. The lesson is not forcing garbage collection; it shows what is guaranteed inside one synchronous job.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const ref = new WeakRef(target);target = null; const first = ref.deref();const second = ref.deref();console.log(first === second);console.log(first?.label ?? "collected");
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Read the code line by line: line 1 creates the object, line 2 creates the weak reference, and line 3 removes the ordinary variable. Lines 5 and 6 call deref() in the same turn, so line 7 prints true. In a later task, both calls must still be treated as possibly undefined.

Use the object right away

Do not store a deref() result for long and assume it represents cache ownership. A local variable is a strong reference while it exists. Weak references are most honest when code reads, checks, uses, and lets go quickly.

What can be weak?

TYPE RULES

Weak targets must be values the garbage collector can track independently. Objects work. Since ES2023, non-registered symbols from Symbol() also work. Registered symbols from Symbol.for() and primitives like numbers, strings, booleans, null, and undefined are not valid targets.

Which values can be weak targets?
Target eligibilityPop out in the code editor (opens in a new tab)JavaScript
const target = { name: "object target" };const ref = new WeakRef(target);console.log(ref.deref() === target);
Resultaccepted
objecttrue
Try it yourself
Choose a target

Objects and non-registered symbols are valid WeakRef targets. deref() returns the same target during this turn.

The code runs the selected case. Registered symbols from Symbol.for() and primitives fail on purpose.
Objects and non-registered symbols are acceptedPop out in the code editor (opens in a new tab)JavaScript
const objectTarget = { name: "object target" };const localSymbol = Symbol("local target"); console.log(new WeakRef(objectTarget).deref() === objectTarget);console.log(new WeakRef(localSymbol).deref() === localSymbol);

The next two examples fail on purpose. The lesson marks them as expected errors so the editor can show the TypeError without treating the example as broken.

Registered symbols are rejectedPop out in the code editor (opens in a new tab)JavaScript
const target = Symbol.for("shared target");new WeakRef(target);
Primitives are rejectedPop out in the code editor (opens in a new tab)JavaScript
new WeakRef(42);

FinalizationRegistry

BEST EFFORT

FinalizationRegistry lets you register a target and a separate held value. If the target is collected and the engine later runs finalizers, your callback receives the held value. That wording is careful because collection and callback scheduling are not deterministic.

FinalizationRegistry pieces
APIMeaning
new FinalizationRegistry(callback)Creates a registry whose callback receives held values after collection, if it runs.
register(target, heldValue, token)Watches an object or non-registered symbol target. heldValue must not be the target.
unregister(token)Removes registrations that used that token and returns whether anything was removed.
Callback timingMay be soon, late, never, or skipped before page unload. Treat it as a hint.
Register, then unregister with a tokenPop out in the code editor (opens in a new tab)JavaScript
const registry = new FinalizationRegistry((heldValue) => {  console.log("cleanup requested for", heldValue);}); const token = {};const widget = { id: "preview" };registry.register(widget, "preview card", token);console.log(registry.unregister(token));console.log(registry.unregister(token));

Line 7 registers widget with a string held value and a token object. Line 8 removes that registration and prints true. Line 9 prints false because there is nothing left for the same token.

The held value must not be the target

A held value is kept so the callback can receive it later. If the held value were the target object itself, the registry would keep the target alive and defeat the whole purpose, so the API throws a TypeError.

Do not use the target as its own held valuePop out in the code editor (opens in a new tab)JavaScript
const registry = new FinalizationRegistry(() => {});const target = {};registry.register(target, target);

Weak caches: Map<key, WeakRef>

STEP THROUGH

A weak cache often uses a normal Map for stable keys such as URLs or file paths, and WeakRef values for large objects such as decoded images or ASTs. A FinalizationRegistry can later remove stale keys, but only after checking the current map entry.

The guard matters because callbacks can be late. A file path might be decoded again before a finalizer for the old object runs. If the callback blindly deletes the key, it can delete fresh work. The helper below calls ref.deref() === undefined before deleting.

Weak cache lab: guard a stale finalizer
Step 0 of 15Ready
Your turn: follow the blue line

A weak cache combines an ordinary Map, WeakRef values, and a registry callback that only deletes a key after checking the current ref.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const registry = new FinalizationRegistry((key) => {  deleteIfStale(key);}); function deleteIfStale(key) {  const ref = records.get(key);  if (ref?.deref() === undefined) {    records.delete(key);  }} function remember(key, value) {  records.set(key, new WeakRef(value));  registry.register(value, key, value);} function read(key) {  const value = records.get(key)?.deref();  if (value === undefined) {    records.delete(key);  }  return value;} remember("app.js", { type: "OldProgram" });remember("app.js", { type: "NewProgram" });// A late finalizer for the old object would call this helper.deleteIfStale("app.js");console.log(read("app.js").type);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

In production, remember would run when you decode or parse a value. read would run when the page asks for the value. The finalizer callback is only a best-effort way to trim stale keys; normal cache reads still need to handle misses.

Why cleanup timing is not guaranteed

POSSIBLE OUTCOMES

Garbage collection is intentionally invisible. An engine may collect late, never collect before a process exits, or skip finalizer callbacks before a page unloads. The TC39 weak references guidance says to avoid these features where possible. Use them only when every missed callback is harmless.

Finalizer timing: choose a possible outcome
Finalizer timing sketchJavaScript
const registry = new FinalizationRegistry((label) => {  console.log("cleanup for", label);}); let node = { label: "preview" };registry.register(node, "preview");node = null; // After this point the callback may run soon, much later, or never.
Possible outcomeCallback runs much later
Also possiblelate
Try it yourself
Pick an outcome after the strong reference is dropped

The engine can delay garbage collection until memory pressure or another scheduling point.

This panel teaches allowed outcomes. It does not ask the browser to collect anything or promise a finalizer will run.

Browser tools can help you experiment manually. In Chrome DevTools, the Memory panel has a Collect garbage button. That is useful for learning and debugging, but it is not something page code can rely on. A demo that says “click this and the object will be collected” is teaching the wrong mental model.

Essential cleanup uses finallyPop out in the code editor (opens in a new tab)JavaScript
const handle = {  close() {    console.log("closed");  },}; try {  console.log("use handle");} finally {  handle.close();}

The same rule applies to explicit resource management. Use try/finally, a documented close() or dispose() method, framework teardown, and in newer JavaScript the using family taught in the next lesson. A finalizer is not a substitute for those paths.

Where these tools are practical

SORT IT

The best uses are optional: caches of big decoded images, parsed ASTs, or formatted data that can be recreated; observers that should not own what they observe; and leak-detection tests where losing a notification fails safe. The wrong uses are essential cleanup tasks.

Practical uses and boundaries
SituationFitReason
Decoded images or parsed ASTsGood weak-cache fitRecreate them when deref() returns undefined; never promise they stay cached.
Observing objects without ownershipPossible fitA monitor can remember a weak target without extending its lifetime.
Leak detection in testsCareful fitA child process with exposed GC can sometimes prove collection, but production code must not depend on it.
Sockets, locks, files, listenersUse explicit cleanupThese resources need deterministic release, not a finalizer that may never run.
Weak cache, finalizer hint, or explicit cleanup?
  • Cache a decoded hero image that can be recreated from a URL.
  • Store parsed ASTs by file path in a developer tool.
  • Notice in a test that an object eventually became collectable.
  • Record approximate telemetry when preview objects disappear.
  • Close a WebSocket when a component unmounts.
  • Remove a DOM event listener added by a widget.
Try it yourself
0 of 6 correct

Sort each task by the safest tool. If a missed callback would break the app, it belongs in explicit cleanup.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
Leak detection tests are special

Some test suites prove collection in a child process with --expose-gc and macrotask waits, then repeat the run many times. That can be valid test engineering, but it is not a production pattern and this lesson does not claim deterministic collection in page code.

Common misconceptions

  • “WeakRef makes collection happen.” It only stops your reference from keeping the target alive. The engine chooses when to collect.
  • “If deref() worked once, it will work later.” The result can be undefined in a later job. Check every time.
  • “Finalizers are a cleanup hook.” They are best-effort notifications. Essential cleanup belongs in explicit code.
  • “The held value can be the target.” It cannot; that would keep the target alive, so the API throws.
  • “A registry callback can blindly delete a cache key.” It must check the current WeakRef, because the key may have been re-added.
  • “WeakRef is a general performance trick.” It is a niche tool. Prefer simpler strong caches with size limits unless weak ownership is the actual requirement.
What to reach for
QuestionWeakMapWeakRefFinalizationRegistryExplicit cleanup
What is weak?Object keysThe target returned by deref()The registered targetNothing; you call it directly
Can I read a value later?Only if I still have the keyMaybe; deref() can be undefinedNo direct read; callback gets holdingsYes; the resource contract decides
Can timing be trusted?No cleanup callback existsNoNoYes, if you call it in the right place

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict two deref reads in one job

Read the code and type the two printed values.

Starter codePop out in the code editor (opens in a new tab)JavaScript
let target = { label: "preview" };
const ref = new WeakRef(target);
target = null;
const a = ref.deref();
const b = ref.deref();
console.log(a === b);
console.log(a?.label ?? "gone");

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeFind the invalid weak target

    The starter prints once and then throws. Which target is rejected?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const local = Symbol("local");
    console.log(new WeakRef(local).deref() === local);
    new WeakRef(Symbol.for("shared"));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeTrace unregister

      Predict the two booleans printed at the end of the registry snippet.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const registry = new FinalizationRegistry((heldValue) => {
        console.log("cleanup requested for", heldValue);
      });
      
      const token = {};
      const widget = { id: "preview" };
      registry.register(widget, "preview card", token);
      console.log(registry.unregister(token));
      console.log(registry.unregister(token));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFix the stale-cache bug

        The broken callback deletes the key blindly. What expression should it check first?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const records = new Map();
        records.set("app.js", { deref: () => ({ type: "NewProgram" }) });
        function finalizerCallback(key) {
          records.delete(key);
        }
        finalizerCallback("app.js");
        console.log(records.has("app.js"));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeChoose cleanup for a real resource

          A page opens a socket or a file-like handle. Which cleanup style is required?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const handle = {
            close() {
              console.log("closed");
            },
          };
          
          try {
            console.log("use handle");
          } finally {
            handle.close();
          }

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            7 QUESTIONS
            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7Which sentence best defines WeakRef?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does the current-job deref() example print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              let target = { label: "decoded image" };
              const ref = new WeakRef(target);
              target = null;
              
              const first = ref.deref();
              const second = ref.deref();
              console.log(first === second);
              console.log(first?.label ?? "collected");

              Choose an answer to see the explanation.

            3. Question 3 of 7What happens when a registered symbol is used as a WeakRef target?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const target = Symbol.for("shared target");
              new WeakRef(target);

              Choose an answer to see the explanation.

            4. Question 4 of 7What does unregistering the same token twice print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const registry = new FinalizationRegistry((heldValue) => {
                console.log("cleanup requested for", heldValue);
              });
              
              const token = {};
              const widget = { id: "preview" };
              registry.register(widget, "preview card", token);
              console.log(registry.unregister(token));
              console.log(registry.unregister(token));

              Choose an answer to see the explanation.

            5. Question 5 of 7Why is this registration invalid?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const registry = new FinalizationRegistry(() => {});
              const target = {};
              registry.register(target, target);

              Choose an answer to see the explanation.

            6. Question 6 of 7What does the guarded weak-cache example print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const records = new Map();
              const registry = new FinalizationRegistry((key) => {
                deleteIfStale(key);
              });
              
              function deleteIfStale(key) {
                const ref = records.get(key);
                if (ref?.deref() === undefined) {
                  records.delete(key);
                }
              }
              
              function remember(key, value) {
                records.set(key, new WeakRef(value));
                registry.register(value, key, value);
              }
              
              function read(key) {
                const value = records.get(key)?.deref();
                if (value === undefined) {
                  records.delete(key);
                }
                return value;
              }
              
              remember("app.js", { type: "OldProgram" });
              remember("app.js", { type: "NewProgram" });
              // A late finalizer for the old object would call this helper.
              deleteIfStale("app.js");
              console.log(read("app.js").type);

              Choose an answer to see the explanation.

            7. Question 7 of 7Which task must not rely on a finalizer?

              Choose an answer to see the explanation.

            Key takeaways

            • WeakRef does not keep a target alive; deref() may return undefined.
            • Creation and deref() keep a target alive only for the current synchronous job.
            • Objects and non-registered symbols can be weak targets; primitives and registered symbols cannot.
            • FinalizationRegistry callbacks are best-effort and must never be essential cleanup.
            • A weak cache deletes stale keys only after checking the current WeakRef.

            Remember the one-liner.
            Weak references are optional observations, not ownership. Use them when losing the value is fine, and clean up real resources explicitly.

            Up next: explicit resource management.

            CompleteFrontend Clear concepts. Working examples.