completefrontend
Open lab My progress
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 safelyMake a new WeakRef(target), read it with deref(), and check for undefined every time.
  • 02
    Ask for a note after an object is removedUse FinalizationRegistry with a held value and a token, and know why the held value must not be the target.
  • 03
    Build a safe weak cacheCombine Map keys, WeakRef values, and a cleanup that checks first, without counting on when callbacks run.

Pointing at an object without keeping it

JavaScript frees memory for you. A part of the engine called the garbage collector finds objects your code can no longer get to, and removes them. An object is reachable while your code can still get to it, for example through a variable, a property, an array, a map, or a closure.

Most links to an object are strong references. A strong reference keeps the object alive: the garbage collector must not remove it. A weak reference is different. It points at an object, but it does not keep the object alive. The thing a weak reference points at is called its target.

The box user has an arrow to an object that holds name: "Asha". This arrow keeps the object alive. The tag ref, a WeakRef, also has an arrow to the same object, but it does not keep the object alive.userstrong: keeps itname: "Asha"objectrefweak: doesn't keep it
The variable user keeps the object alive. The WeakRef called ref points at the same object, but it does not keep it alive.
The main idea

A WeakRef is a weak reference to an object, or to a symbol made with Symbol(). Its deref() method gives you the target if it still exists. It gives undefined if the garbage collector has removed it.

A FinalizationRegistry can run a function, called a callback, after a target is removed. But nothing promises when that callback runs. It may run late, or never.

Don't mix these up with WeakMap and WeakSet:

  • A WeakMap holds its keys weakly. An object used as a key can be removed, and the map doesn't stop that.
  • A WeakRef holds one target weakly. You read the target with deref().

Both follow the reachability rules from the memory management lesson.

Real-life analogyA photo of a friend

A photo lets you see your friend. It does not make your friend stay in the room. To know if your friend is still here, you have to look around and check.

In real life: A photo shows your friend
In JavaScript: A WeakRef points at a target
In real life: The photo doesn't make your friend stay
In JavaScript: The weak reference doesn't keep the target alive
In real life: You look around to see if your friend is still here
In JavaScript: You call deref() and check for undefined

The table below compares four tools that are easy to mix up. Read each row as: "this tool holds this weakly, and here is a good first use for it."

Four tools that are easy to mix up
ToolWhat does it hold weakly?A good first use
WeakMapIts keys. The values are normal.Attach extra data to an object without keeping the object alive.
WeakRefOne target, which you read with deref().Point at a big object, and ask deref() later if it still exists.
FinalizationRegistryThe targets you register with it.Get a note (the held value) after a target is removed, if the engine sends it.
Explicit cleanupNothing. You run the cleanup yourself.Close files and connections with try/finally, .close(), .dispose(), or using.

This lesson also uses ideas from two earlier lessons. From closures: a closure can keep a value alive by accident. From symbols: some symbols can be weak targets. The next lesson, resource management, shows cleanup you control, such as using.

WeakRef and deref()

STEP THROUGH

To make a weak reference, write new WeakRef(target). To read it, call ref.deref(). Then check what you got:

  • If it is the object, use it right away.
  • If it is undefined, the object is gone. Build it again, or treat it as missing.

The examples use two small tools for this check. ?. reads a property, but gives undefined instead of an error when the left side is undefined. ?? gives the value on its right when the left side is undefined or null.

Read a target while it still existsPop out in the code editor (opens in a new tab)JavaScript
const user = { name: "Asha" };
const ref = new WeakRef(user);
console.log(ref.deref()?.name ?? "missing");

Line 1 makes an object and keeps it in the variable user. That is a strong reference. Line 2 makes a weak reference to the same object. Line 3 prints Asha, because user still keeps the object alive. The ?? "missing" part is for a later read, when the object might be gone. It doesn't mean the object is removed now.

Handle a read that finds nothingPop out in the code editor (opens in a new tab)JavaScript
function readName(ref) {
  const user = ref.deref();
  return user?.name ?? "reload";
}
console.log(readName({ deref: () => undefined }));

Line 2 calls deref() once and keeps the answer in user. Line 3 returns the name, or reload if there is no user. Line 5 prints reload.

Why? Line 5 doesn't pass a real WeakRef. It passes a small stand-in object whose deref() always returns undefined. That shows you the missing case. It does not make the garbage collector remove anything.

Calling ref.deref() gives one of two answers. Top: the object with name: "Asha", while it is still alive. Bottom: undefined, after the object was collected.ref.deref()ref{ name: "Asha" }still aliveundefinedwas collected
deref() gives you the object while it is still alive, and undefined after it is collected. So check the answer every time.
Check again laterPop out in the code editor (opens in a new tab)JavaScript
function readLater(ref) {
  setTimeout(() => {
    const user = ref.deref();
    console.log(user?.name ?? "reload");
  }, 0);
}
readLater({ deref: () => undefined });

A job is one piece of code that runs from start to finish without a break. Code inside a timer runs later, in a new job.

Line 2 uses setTimeout to run some code later, in a new job. Line 3 calls deref() again. Line 4 prints reload, because this stand-in returns undefined. With a real WeakRef, the object might still be there, or it might be gone. A read that worked earlier tells you nothing about a later job.

The next experiment shows a promise from the specification (the spec), the official rulebook for JavaScript. When you create a WeakRef or call deref(), the spec keeps the target alive until the current job ends. The spec calls this rule KeepDuringJob. It does not keep the object forever.

Step through: what does deref() give in one job?
Step 0 of 7Ready
Your turn: follow the blue line

Step through a WeakRef read. Nothing here makes the garbage collector run. It shows what is promised inside one job.

Running in
  1. script
Next: line 1
const ref = new WeakRef(target);target = null; const first = ref.deref();const second = ref.deref();console.log(first === second);console.log(first?.label ?? "collected");
CallStoreChangeResultBlue line: runs next. 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.

Line 1 makes the object. Line 2 makes the weak reference. Line 3 sets target to null, which removes the strong reference. Lines 5 and 6 call deref() in the same job, so both get the same object. That is why line 7 prints true. In a later job, either call could give undefined.

Use the object right away

Don't keep a deref() answer for a long time. While a variable holds the object, that variable is a strong reference, so it keeps the object alive. The safe pattern is short: read, check, use, and let go.

What can be weak?

TYPE RULES

A weak target must be something the garbage collector can remove on its own. These work:

  • Objects.
  • Symbols made with Symbol(). These are called non-registered symbols. They work since ES2023, the 2023 edition of JavaScript.

These don't work. new WeakRef() throws a TypeError for them:

  • Registered symbols from Symbol.for(). JavaScript keeps them in one shared list, and Symbol.for() with the same name always gives back the same symbol.
  • Primitives: numbers, strings, booleans, null, and undefined.

Older engines may not accept symbols as targets. If your code needs symbol targets, check once when the program starts. If the check fails, use an object target instead.

Check if symbols can be targetsPop out in the code editor (opens in a new tab)JavaScript
const local = Symbol("local");
let supported = false;
try { supported = new WeakRef(local).deref() === local; }
catch { supported = false; }
console.log(supported ? "symbol target supported" : "use an object target");

Line 3 tries a symbol as a target, inside a try block. It checks that deref() gives the same symbol back. If an older engine throws, the catch on line 4 runs instead. On current Node 22, line 5 prints symbol target supported. An older engine that rejects symbols prints use an object target instead.

Which values can be weak targets?
Try a targetPop 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);
Resultworks
objecttrue
Try it yourself
Choose a target

Objects and Symbol() symbols can be WeakRef targets. Here deref() gives back the same target.

The code runs the case you pick. Symbols from Symbol.for() and primitives fail on purpose.
Objects and Symbol() symbols workPop 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);

Line 1 makes an object. Line 2 makes a non-registered symbol. Lines 4 and 5 make a weak reference to each one. Both print true, because deref() gives back the same target.

The next two examples fail on purpose. They are marked as expected errors, so the editor shows the TypeError and doesn't treat the example as broken.

Symbol.for() 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 ask for a note after an object is removed. You register two things with it:

  • The target: the object to watch.
  • A held value: a small value, such as a string, that the registry keeps for you.

If the target is removed, the engine may later call your callback with the held value. This callback is also called a finalizer. You can't control when the target is removed, or when the finalizer runs.

The table lists each part of FinalizationRegistry and what it does.

The parts of FinalizationRegistry
PartWhat it does
new FinalizationRegistry(callback)Makes a registry. If the callback runs, it gets a held value after a target is removed.
register(target, heldValue, token)Starts watching a target: an object or a Symbol() symbol. heldValue must not be the target. token is optional.
unregister(token)Cancels every registration made with that token. Returns true if it cancelled any, and false if not.
When the callback runsMaybe soon, maybe late, maybe never, and maybe not before the page closes. 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));

Lines 1 to 3 make the registry. Its callback prints the held value. Line 5 makes an empty object called token. A token is a key you can use later to cancel a registration.

Line 7 registers widget, with the held value "preview card" and the token. Line 8 cancels that registration and prints true. Line 9 prints false, because nothing is left for that token. The callback never runs here.

The widget object is the target. It goes into the registry, which keeps the held value "preview card". An arrow from the registry leads to the callback function, which may run later, or never.widgetthe target"preview card"registrycallbackruns later, or never
The registry watches widget and keeps the held value, "preview card". After widget is collected, the callback may get that value: maybe late, maybe never.

A token is optional. If several registrations share one token, a single unregister(token) cancels them all. Cancelling never runs the callback.

One token cancels several registrationsPop out in the code editor (opens in a new tab)JavaScript
const registry = new FinalizationRegistry(() => {});
const token = {};
const first = { name: "Asha" };
const second = { name: "Mira" };
registry.register(first, "first", token);
registry.register(second, "second", token);
console.log(registry.unregister(token));
console.log(registry.unregister(token));

Lines 5 and 6 register two users with the same token. Line 7 cancels both and prints true. Line 8 prints false, because none are left. The garbage collector didn't remove either user here.

The held value must not be the target

The registry keeps the held value, so it can pass it to the callback later. If the held value were the target itself, the registry would keep the target alive. Then the target could never be removed. So register() throws a TypeError.

Wrapping the target in another object is a trap too. For example, { user: target } as the held value doesn't throw, but it still keeps the target alive. Keep held values small, such as a string ID. Also, don't let the callback function use the target.

Don't 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);
A wrapped target doesn't throw, but stays alivePop out in the code editor (opens in a new tab)JavaScript
const registry = new FinalizationRegistry(() => {});
let user = { name: "Asha" };
registry.register(user, { savedUser: user });
user = null;
console.log("registered, but user is still strongly held");

Line 3 works, because the held value is a new object, not user itself. Line 4 removes the variable's strong reference. Line 5 prints registered, but user is still strongly held.

Here is why. The registry keeps the held value, and the held value points back at the user. While that path exists, the user can't be removed, so the callback can never run.

Weak caches: Map<key, WeakRef>

STEP THROUGH

A cache keeps results you made before, so you don't have to make them again. A weak cache usually has two parts:

  • A normal Map. Its keys are simple values that don't change, such as URLs or file paths.
  • WeakRef values that point at big objects, such as decoded images or ASTs. An AST (abstract syntax tree) is a program turned into a tree of objects.

A FinalizationRegistry can remove old keys later. But it must check the current map entry first. You'll see why below.

A key that was never cachedPop out in the code editor (opens in a new tab)JavaScript
const records = new Map();
console.log(records.get("tea")?.deref() ?? "not cached");

Line 1 starts with an empty map. Line 2 prints not cached, because there is no entry to call deref() on. Your code needs a fallback both for a missing key and for a weak reference that finds nothing.

A weak reference can find nothingPop out in the code editor (opens in a new tab)JavaScript
const records = new Map([["tea", { deref: () => undefined }]]);
const cached = records.get("tea")?.deref();
console.log(cached?.label ?? "cache miss");

This time the key exists, but the stand-in on line 1 returns undefined. Line 3 prints cache miss. A cache miss means the cache didn't have the value. This shows what a removed object looks like, without making the garbage collector run.

Build a missing value againPop out in the code editor (opens in a new tab)JavaScript
const records = new Map([["tea", { deref: () => undefined }]]);
function getTea() {
  const cached = records.get("tea")?.deref();
  if (cached !== undefined) return cached;
  const tea = { label: "fresh tea" };
  records.set("tea", new WeakRef(tea));
  return tea;
}
console.log(getTea().label);

Line 3 checks the old entry. It finds nothing, so line 4 doesn't return. Lines 5 and 6 make a new value and store a new WeakRef to it. Line 9 prints fresh tea. If a later read also misses, build the value again. The map alone doesn't promise that a value stays cached.

Now, why must the cleanup check first? Because a callback can arrive late:

  1. The old object for app.js is removed.
  2. Before its callback runs, your code stores a fresh value under app.js.
  3. The late callback for the old object arrives.

If the callback deletes the key without checking, it deletes the fresh value. So the helper below first checks ref.deref() === undefined. It deletes the key only when that is true.

Two carts called records, after a late cleanup for the old app.js object. Left: the cleanup deletes without checking, so the cart is empty and the fresh app.js entry is lost. Right: the cleanup checks deref() first, so the app.js entry is kept.deletes without checkingrecordsfresh entry lostchecks deref() firstapp.jsrecordsfresh entry kept
A late cleanup for the old object reaches the app.js key after it holds a fresh value. Deleting without checking loses it; checking deref() first keeps it.
Step through: a cleanup that checks first
Step 0 of 15Ready
Your turn: follow the blue line

A weak cache uses a normal Map, WeakRef values, and a registry callback that deletes a key only after it checks the current ref.

Running in
  1. script
Next: line 1
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);
CallStoreChangeResultBlue line: runs next. 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.

Lines 26 and 27 store an old value and then a new value under app.js. Line 29 calls the cleanup helper, the way a late finalizer for the old value would. On line 8, deref() finds the new value, so the key stays. Line 30 prints NewProgram.

In a real app, remember runs when you decode or parse a value. read runs when the page asks for that value. The registry callback is only a best-effort way to remove old keys: it may happen, but nothing promises it. So normal reads must still handle a miss.

If callbacks never run, old string keys can pile up in the normal Map. Remove them when a read misses. If the number of keys can keep growing, also set a size limit, or clear old keys yourself from time to time. Weak values don't make the map's keys weak.

Why cleanup timing is never promised

POSSIBLE OUTCOMES

Your code can't see garbage collection, and it can't control it. An engine may:

  • remove an object late,
  • never remove it before the program ends, or
  • skip the finalizers when a page closes.

TC39, the group that designs JavaScript, advises you to avoid these features when you can. Use them only when a missed callback does no harm.

When does the finalizer run? Pick what could happen
A finalizer with no promised timeJavaScript
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.
What could happenCallback runs much later
Can also happenlate
Try it yourself
After line 7 removes the strong reference, what could happen?

The engine may wait to collect garbage until it runs low on memory, or until some other moment it picks.

Each choice is something the rules allow. This panel doesn't ask the browser to remove anything, and it doesn't promise a finalizer will run.

Browser tools let you try this by hand. In Chrome DevTools, the Memory panel has a Collect garbage button. It is useful for learning and for finding bugs. But your page's code can't press that button. So a demo that says "click here and the object will be removed" teaches the wrong idea.

For cleanup that must happen, use explicit cleanup: cleanup code that you call yourself, at a time you choose.

Important cleanup goes in 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();}

Lines 1 to 5 make a handle object with a close() method. Line 7 starts a try block, and line 8 prints use handle. The finally block on line 9 always runs after the try block. So line 10 closes the handle and prints closed. This also works when the work fails.

Cleanup still runs when the work throwsPop out in the code editor (opens in a new tab)JavaScript
const handle = { close: () => console.log("closed") };
try {
  try { throw new Error("failed"); }
  finally { handle.close(); }
} catch (error) {
  console.log(error.message);
}

Line 3 throws an error. The finally on line 4 still runs and closes the handle, which prints closed. Then the outer catch on line 5 catches the error, and line 6 prints failed. Without that outer catch, the error would go on to the calling code, after the cleanup.

Use explicit cleanup for anything important:

  • try/finally.
  • A close() or dispose() method that you call.
  • Your framework's teardown step, which runs when a component is removed.
  • In newer JavaScript, using, which the next lesson teaches.

A finalizer can't replace any of these.

Where these tools are useful

SORT IT

These tools fit jobs where losing the value is fine:

  • Caches of big things you can make again, such as decoded images, ASTs, or formatted data.
  • Code that watches an object but should not keep it alive.
  • Tests that look for memory leaks, where a missed note does no harm. A memory leak is memory your program keeps by mistake.

They don't fit cleanup that must happen.

The table lists common situations. For each one, it says whether these tools fit, and why.

Where weak tools fit, and where they don't
SituationDo they fit?Why
Decoded images or ASTsYes, as a weak cache.When deref() gives undefined, make the value again. Never promise it stays cached.
Watching an object you don't ownSometimes.Your code can point at the object without keeping it alive.
Tests that look for memory leaksWith care.A test process that can ask for garbage collection may see an object removed. App code must not depend on it.
Sockets, locks, files, event listenersNo. Use explicit cleanup.These must be released for sure, not by a finalizer that may never run.
Weak cache, finalizer, or explicit cleanup?
  • Cache a big decoded image that you can make again from its URL.
  • Store ASTs by file path in a developer tool.
  • Notice in a test that an object was finally removed.
  • Roughly count, for statistics, when preview objects go away.
  • Close a WebSocket when a component is removed from the page.
  • Remove a page event listener that a widget added.
Try it yourself
0 of 6 correct

Put each task under the safest tool. If a missed callback would break the app, it needs explicit cleanup.

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

A test can run in its own Node process with the --expose-gc flag. That flag lets the test ask for garbage collection, and then check whether a target was removed.

Even then, seeing nothing by a deadline doesn't prove a leak. Removal and finalizers are never promised. Code that runs in a page must never depend on either one.

Common misconceptions

  • "WeakRef makes the object get removed." It only stops your reference from keeping the target alive. The engine decides when to remove it.
  • "If deref() worked once, it will work later." In a later job it can give undefined. Check every time.
  • "A finalizer is a cleanup hook." A finalizer is a best-effort note: it may come late, or never. Important cleanup belongs in code you call yourself.
  • "The held value can be the target." It can't. That would keep the target alive, so register() throws a TypeError.
  • "A registry callback can just delete its cache key." It must check the current WeakRef first, because the key may hold a fresh value by then.
  • "WeakRef makes code faster." It is a tool for rare cases. A normal cache with a size limit is usually simpler. Use WeakRef only when the object really must stay removable.

The table compares the four tools side by side. Each row asks one question, and each column answers it for one tool.

Which tool to use: four tools compared
QuestionWeakMapWeakRefFinalizationRegistryExplicit cleanup
What does it hold weakly?Its object keys.The target that deref() returns.The targets you register.Nothing. You run it yourself.
Can I read the value later?Yes, while I still have the key.Maybe. deref() can give undefined.No. The callback gets the held value instead.Yes. The resource's own methods decide.
Can I trust the timing?There is no callback to wait for.No. The target can go at any time.No. The callback may come late, or never.Yes, if I call it in the right place.

Practice exercises

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

Read the code. Type the two values it prints.

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 target that is rejected

    The code 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 values, true or false, that the last two lines print.

      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 cleanup that deletes too much

        The broken callback deletes the key without checking, so the code prints false. What 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 (a live network connection) or a handle to a file. Which kind of cleanup does it need?

          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 describes WeakRef?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this code 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 Symbol.for() 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 calling unregister(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 does this registration throw?

              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 this weak cache code 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 depend on a finalizer?

              Choose an answer to see the explanation.

            Key takeaways

            • WeakRef doesn't keep its target alive. So deref() can return undefined.
            • Creating a WeakRef or calling deref() keeps the target alive only until the current job ends.
            • Objects and Symbol() symbols can be weak targets. Primitives and Symbol.for() symbols can't.
            • FinalizationRegistry callbacks may run late, or never. Never use them for cleanup that must happen.
            • A weak cache deletes an old key only after checking the current WeakRef.

            Remember the one-liner.
            Weak references let you look, not keep. Use them when losing the value is fine, and clean up real resources yourself.

            Up next: explicit resource management.

            CompleteFrontend Clear concepts. Working examples.