JavaScript Easy technical 1 views 4 min read

What are WeakRef and FinalizationRegistry used for?

Peer-reviewed by HireXTech Technical Panel Updated for 2025/2026 hiring Editorial standards
Practise this track
Interviewer Expectations for this Question
01
Core Competency

Assesses fundamental understanding of JavaScript conventions, runtime behavior, and memory/performance considerations.

02
Evaluation Criteria

Hiring managers look for precision, avoidance of ambiguous jargon, and ability to explain trade-offs under real production conditions.

Comprehensive Model Answer Verified Solution

WeakRef and FinalizationRegistry are advanced features (introduced in ES2021) for managing memory and object lifecycles in JavaScript. They provide low-level control over garbage collection behavior.

WeakRef (Weak Reference):

A WeakRef creates a weak reference to an object, meaning it doesn't prevent the object from being garbage collected. Unlike regular references, holding a WeakRef doesn't keep the object alive.

Syntax:

     const weakRef = new WeakRef(targetObject);
     const obj = weakRef.deref(); // Get the object (or undefined if collected)
     

WeakRef Examples:

     // Creating a weak reference
     let obj = { name: 'Important Data', value: 42 };
     const weakRef = new WeakRef(obj);

     // Access the object
     console.log(weakRef.deref()); // { name: 'Important Data', value: 42 }

     // Remove strong reference
     obj = null;

     // At some point, after garbage collection
     console.log(weakRef.deref()); // undefined (object was collected)

     // Practical use: Caching without memory leaks
     class ImageCache {
       #cache = new Map();

       getImage(url) {
         const weakRef = this.#cache.get(url);
         if (weakRef) {
           const image = weakRef.deref();
           if (image) {
             console.log('Cache hit!');
             return image;
           }
         }

         // Load image if not in cache or was collected
         console.log('Cache miss, loading...');
         const newImage = this.loadImage(url);
         this.#cache.set(url, new WeakRef(newImage));
         return newImage;
       }

       loadImage(url) {
         // Simulate loading
         return { url, data: `Image data for ${url}` };
       }
     }

     const cache = new ImageCache();
     const img1 = cache.getImage('photo.jpg'); // Cache miss
     const img2 = cache.getImage('photo.jpg'); // Cache hit!
     

FinalizationRegistry:

FinalizationRegistry allows you to register callbacks that run after objects are garbage collected. This enables cleanup actions when objects are no longer needed.

Syntax:

     const registry = new FinalizationRegistry((heldValue) => {
       // Cleanup callback when object is garbage collected
       console.log('Cleaning up:', heldValue);
     });

     registry.register(targetObject, heldValue, unregisterToken);
     

FinalizationRegistry Examples:

     // Basic usage
     const registry = new FinalizationRegistry((filename) => {
       console.log(`File ${filename} can be deleted - object was collected`);
       // Perform cleanup: close file handles, free resources, etc.
     });

     let fileObject = { name: 'temp.txt', handle: 'handle123' };
     registry.register(fileObject, 'temp.txt');

     // When fileObject is garbage collected, the callback runs
     fileObject = null; // Remove strong reference

     // Real-world example: Resource management
     class FileManager {
       #registry = new FinalizationRegistry((filepath) => {
         this.#closeFile(filepath);
       });

       #openFiles = new Map();

       openFile(filepath) {
         const handle = this.#actuallyOpenFile(filepath);
         const file = { filepath, handle };
         
         this.#openFiles.set(filepath, handle);
         this.#registry.register(file, filepath);
         
         return file;
       }

       #actuallyOpenFile(filepath) {
         console.log(`Opening ${filepath}`);
         return { /* file handle */ };
       }

       #closeFile(filepath) {
         const handle = this.#openFiles.get(filepath);
         if (handle) {
           console.log(`Auto-closing ${filepath}`);
           // Close file handle
           this.#openFiles.delete(filepath);
         }
       }
     }

     // Database connection pooling
     class ConnectionPool {
       #registry = new FinalizationRegistry((connectionId) => {
         console.log(`Connection ${connectionId} released`);
         this.#releaseConnection(connectionId);
       });

       #connections = new Map();

       getConnection() {
         const connectionId = Math.random().toString(36);
         const connection = { id: connectionId, query: () => {} };
         
         this.#connections.set(connectionId, connection);
         this.#registry.register(connection, connectionId);
         
         return connection;
       }

       #releaseConnection(connectionId) {
         this.#connections.delete(connectionId);
         // Return connection to pool
       }
     }

     // Using unregister token to prevent cleanup
     const cleanupRegistry = new FinalizationRegistry((msg) => {
       console.log('Cleanup:', msg);
     });

     let importantObj = { data: 'important' };
     const token = {}; // Unregister token

     cleanupRegistry.register(importantObj, 'important data', token);

     // Later, if you want to prevent cleanup
     cleanupRegistry.unregister(token); // Callback won't run even after GC
     

Combined Example - Cache with Cleanup:

     class SmartCache {
       #cache = new Map();
       #registry = new FinalizationRegistry((key) => {
         console.log(`Removing cache entry: ${key}`);
         this.#cache.delete(key);
       });

       set(key, value) {
         const weakRef = new WeakRef(value);
         this.#cache.set(key, weakRef);
         this.#registry.register(value, key, weakRef);
       }

       get(key) {
         const weakRef = this.#cache.get(key);
         if (!weakRef) return undefined;

         const value = weakRef.deref();
         if (value === undefined) {
           // Object was collected, clean up map
           this.#cache.delete(key);
         }
         return value;
       }

       has(key) {
         return this.get(key) !== undefined;
       }

       delete(key) {
         const weakRef = this.#cache.get(key);
         if (weakRef) {
           this.#registry.unregister(weakRef);
           this.#cache.delete(key);
         }
       }
     }

     const cache = new SmartCache();
     let data = { huge: 'dataset' };
     cache.set('myData', data);
     
     console.log(cache.get('myData')); // { huge: 'dataset' }
     
     data = null; // Remove strong reference
     // After GC, cache entry is automatically cleaned up
     

Important Caveats:

  1. Non-Deterministic: Garbage collection timing is unpredictable
  2. No Guarantees: The finalization callback may never run (e.g., if the process exits)
  3. Performance: These are advanced features; use only when necessary
  4. Avoid Over-Use: Regular JavaScript patterns are usually better
  5. Not for Critical Logic: Don't rely on finalization for business logic

When to Use:

  • ✅ Caching large objects that can be recreated
  • ✅ Managing native resources (file handles, sockets)
  • ✅ Automatic cleanup of external resources
  • ✅ Memory-sensitive applications
  • ❌ Not for regular object lifecycle management
  • ❌ Not for critical cleanup (use explicit cleanup instead)

Candidate Response Strategy & Interview Tips

  1. Start with a concise one-sentence summary: Deliver a direct, confident answer first before expanding into nuances.
  2. Demonstrate real-world trade-offs: Discuss where this approach excels and when you would avoid it in production systems.
  3. Discuss complexity & edge cases: Proactively explain time/space complexity or boundary conditions (null values, scale limits).
  4. Prepare for interviewer follow-ups: Technical hiring panels frequently probe deeper into concurrency, backward compatibility, or alternative libraries.
Related Topics & Skills
Spotted an error or have an alternative solution?