Advanced Patterns
WeakRef, AbortController, Symbols, Iterators, Generators, and Broadcast Channel — advanced JavaScript patterns for memory, control flow, and communication
Advanced Patterns
Advanced JavaScript features that solve specific, often overlooked problems — memory management, cancellation, lazy evaluation, cross-context communication, and meta-programming primitives.
WeakRef & FinalizationRegistry
Problem
Standard references prevent garbage collection. Caches and object mappings can cause memory leaks when they hold strong references to objects that are no longer needed.
WeakRef
Creates a reference that does not prevent garbage collection:
class Cache {
#entries = new Map();
set(key, value) {
this.#entries.set(key, new WeakRef(value));
}
get(key) {
const ref = this.#entries.get(key);
if (!ref) return undefined;
const value = ref.deref(); // Returns undefined if GC'd
if (!value) {
this.#entries.delete(key); // Clean up stale entry
return undefined;
}
return value;
}
}
const cache = new Cache();
let hugeData = { /* large object */ };
cache.set('report', hugeData);
hugeData = null; // Object can now be garbage collected
// cache.get('report') may return undefined after GC runsFinalizationRegistry
Runs a callback after an object is garbage collected:
const registry = new FinalizationRegistry((heldValue) => {
console.log(`Object with ID ${heldValue} was garbage collected`);
// Cleanup: close file handles, release native resources, update maps
});
function trackObject(id, obj) {
registry.register(obj, id); // heldValue = id, sent to callback on GC
}
let obj = { data: 'important' };
trackObject('obj-1', obj);
obj = null; // Eventually logs: "Object with ID obj-1 was garbage collected"DOM Element Cache with Auto-Cleanup
class DOMCache {
#cache = new Map();
#registry = new FinalizationRegistry((key) => {
this.#cache.delete(key);
});
store(key, element, metadata) {
this.#cache.set(key, {
ref: new WeakRef(element),
metadata,
});
this.#registry.register(element, key);
}
get(key) {
const entry = this.#cache.get(key);
if (!entry) return null;
const element = entry.ref.deref();
if (!element) {
this.#cache.delete(key);
return null;
}
return { element, metadata: entry.metadata };
}
}When to Use
| Use Case | Recommendation |
|---|---|
| LRU cache for expensive computations | WeakRef for cache values |
| Object-to-metadata mapping | WeakMap (preferred) or WeakRef |
| Releasing native/external resources | FinalizationRegistry |
| Preventing memory leaks in long-lived maps | WeakRef + FinalizationRegistry |
| Critical cleanup logic | Do not rely on FinalizationRegistry — GC timing is not guaranteed |
AbortController
Problem
No standard way to cancel async operations — fetch requests, event listeners, timers, or custom async work.
Basic Usage
const controller = new AbortController();
const { signal } = controller;
// Fetch with cancellation
fetch('/api/data', { signal })
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('Request was cancelled');
} else {
throw err;
}
});
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);Timeout Helper
function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
return fetch(url, { ...options, signal: controller.signal })
.finally(() => clearTimeout(timeout));
}
// Built-in alternative (newer browsers)
fetch('/api/data', { signal: AbortSignal.timeout(5000) });Cancel Previous Request (Search-as-you-type)
function createSearchHandler(endpoint) {
let currentController = null;
return async function search(query) {
// Cancel previous in-flight request
currentController?.abort();
currentController = new AbortController();
try {
const res = await fetch(`${endpoint}?q=${encodeURIComponent(query)}`, {
signal: currentController.signal,
});
return await res.json();
} catch (err) {
if (err.name === 'AbortError') return null; // Cancelled, ignore
throw err;
}
};
}
const search = createSearchHandler('/api/search');
input.addEventListener('input', async (e) => {
const results = await search(e.target.value);
if (results) renderResults(results);
});Abort Event Listeners
const controller = new AbortController();
// All listeners removed when controller.abort() is called
document.addEventListener('click', handleClick, { signal: controller.signal });
document.addEventListener('keydown', handleKey, { signal: controller.signal });
window.addEventListener('resize', handleResize, { signal: controller.signal });
// One call removes all three listeners
controller.abort();Composing Signals
// AbortSignal.any() — abort when ANY signal fires
const userCancel = new AbortController();
const timeout = AbortSignal.timeout(10000);
fetch('/api/data', {
signal: AbortSignal.any([userCancel.signal, timeout])
});
// Cancels if user clicks cancel OR after 10 seconds
cancelButton.onclick = () => userCancel.abort();Custom Abortable Operations
async function processItems(items, signal) {
const results = [];
for (const item of items) {
// Check cancellation before each unit of work
if (signal?.aborted) {
throw new DOMException('Operation cancelled', 'AbortError');
}
const result = await processItem(item);
results.push(result);
}
return results;
}
// Also listen for abort during async work
async function longRunningTask(signal) {
return new Promise((resolve, reject) => {
signal?.addEventListener('abort', () => {
reject(new DOMException('Aborted', 'AbortError'));
});
// ... do async work ...
});
}Symbols
Problem
No way to create truly unique property keys. No mechanism to customize built-in language behavior for custom objects.
Unique Property Keys
const id = Symbol('id');
const secretId = Symbol('id'); // Different symbol despite same description
const user = {
name: 'Alice',
[id]: 12345,
};
user[id]; // 12345
user[secretId]; // undefined — different symbol
// Symbols are hidden from normal enumeration
Object.keys(user); // ['name']
JSON.stringify(user); // '{"name":"Alice"}'
Object.getOwnPropertySymbols(user); // [Symbol(id)]Well-Known Symbols
Customize how objects interact with built-in JavaScript operations:
// Symbol.iterator — make an object iterable
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
return current <= end
? { value: current++, done: false }
: { done: true };
}
};
}
}
const range = new Range(1, 5);
[...range]; // [1, 2, 3, 4, 5]
for (const n of range) {} // Iterates 1 through 5// Symbol.toPrimitive — control type coercion
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number': return this.amount;
case 'string': return `${this.amount} ${this.currency}`;
default: return this.amount;
}
}
}
const price = new Money(99.99, 'USD');
+price; // 99.99
`${price}`; // "99.99 USD"
price + 1; // 100.99// Symbol.hasInstance — customize instanceof
class EvenNumber {
static [Symbol.hasInstance](num) {
return typeof num === 'number' && num % 2 === 0;
}
}
4 instanceof EvenNumber; // true
5 instanceof EvenNumber; // falseKey Well-Known Symbols
| Symbol | Customizes | Example |
|---|---|---|
Symbol.iterator | for...of, spread, destructuring | Make objects iterable |
Symbol.asyncIterator | for await...of | Async iteration |
Symbol.toPrimitive | Type coercion | Custom number/string conversion |
Symbol.hasInstance | instanceof | Custom type checks |
Symbol.toStringTag | Object.prototype.toString() | Custom [object Tag] output |
Symbol.species | Constructor for derived objects | Control map(), filter() return type |
Symbol.for — Global Symbol Registry
// Shared symbols across modules/iframes/realms
const key = Symbol.for('app.config');
const sameKey = Symbol.for('app.config');
key === sameKey; // true — same symbol from global registry
Symbol.keyFor(key); // 'app.config'Iterators & Generators
Problem
No lazy evaluation pattern. Processing large datasets requires loading everything into memory. No standard way to produce values on demand.
Custom Iterator
function createPaginator(fetchPage) {
return {
[Symbol.asyncIterator]() {
let page = 1;
let done = false;
return {
async next() {
if (done) return { done: true };
const data = await fetchPage(page);
if (data.length === 0) {
done = true;
return { done: true };
}
page++;
return { value: data, done: false };
}
};
}
};
}
// Usage — processes one page at a time
const pages = createPaginator((page) => fetch(`/api/items?page=${page}`).then(r => r.json()));
for await (const items of pages) {
renderItems(items);
}Generators
// Lazy range — produces values on demand, no array allocated
function* range(start, end, step = 1) {
for (let i = start; i <= end; i += step) {
yield i;
}
}
for (const n of range(1, 1000000)) {
if (n > 5) break; // Only generates values 1-6, not all million
}
// Infinite sequence
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// Take first 10 Fibonacci numbers
function take(n, iterable) {
const result = [];
for (const item of iterable) {
result.push(item);
if (result.length >= n) break;
}
return result;
}
take(10, fibonacci()); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]Async Generators
// Stream processing — read and process chunks as they arrive
async function* readStream(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
yield decoder.decode(value, { stream: true });
}
}
for await (const chunk of readStream('/api/large-file')) {
processChunk(chunk);
}// Polling with backoff
async function* poll(fn, { interval = 1000, maxInterval = 30000, signal } = {}) {
let delay = interval;
while (!signal?.aborted) {
try {
const result = await fn();
yield result;
delay = interval; // Reset on success
} catch {
delay = Math.min(delay * 2, maxInterval); // Backoff on failure
}
await new Promise(resolve => setTimeout(resolve, delay));
}
}
// Usage
const controller = new AbortController();
for await (const status of poll(() => fetchStatus(), { signal: controller.signal })) {
updateUI(status);
if (status.complete) {
controller.abort();
}
}Generator as State Machine
function* trafficLight() {
while (true) {
yield 'green';
yield 'yellow';
yield 'red';
}
}
const light = trafficLight();
light.next().value; // 'green'
light.next().value; // 'yellow'
light.next().value; // 'red'
light.next().value; // 'green' (cycles)Broadcast Channel
Problem
No simple way to send messages between same-origin tabs, windows, or iframes.
Basic Usage
// Tab 1
const channel = new BroadcastChannel('app-events');
channel.postMessage({
type: 'USER_LOGOUT',
timestamp: Date.now(),
});
// Tab 2 (same origin)
const channel = new BroadcastChannel('app-events');
channel.onmessage = (event) => {
if (event.data.type === 'USER_LOGOUT') {
// Redirect to login page
window.location.href = '/login';
}
};
// Cleanup
channel.close();Cross-Tab State Sync
class CrossTabState {
#channel;
#state;
#listeners = new Set();
constructor(channelName, initialState = {}) {
this.#state = initialState;
this.#channel = new BroadcastChannel(channelName);
this.#channel.onmessage = (event) => {
const { key, value } = event.data;
this.#state[key] = value;
this.#listeners.forEach(fn => fn(key, value));
};
}
get(key) {
return this.#state[key];
}
set(key, value) {
this.#state[key] = value;
this.#channel.postMessage({ key, value });
this.#listeners.forEach(fn => fn(key, value));
}
onChange(fn) {
this.#listeners.add(fn);
return () => this.#listeners.delete(fn);
}
destroy() {
this.#channel.close();
this.#listeners.clear();
}
}
// Usage — syncs theme across all tabs
const state = new CrossTabState('app-state', { theme: 'light' });
state.onChange((key, value) => {
if (key === 'theme') document.documentElement.dataset.theme = value;
});
state.set('theme', 'dark'); // All tabs switch to dark modeUse Cases
| Use Case | Example |
|---|---|
| Logout sync | One tab logs out → all tabs redirect to login |
| Theme/locale sync | User changes theme → all tabs update |
| Cart updates | Item added in one tab → badge updates in all tabs |
| Leader election | One tab coordinates background work |
| Cache invalidation | One tab detects stale data → all tabs refresh |
Structured Clone & Transferable Objects
Problem
JSON.parse(JSON.stringify()) cannot handle Date, Map, Set, RegExp, ArrayBuffer, circular references, or undefined.
structuredClone
const original = {
date: new Date(),
pattern: /test/gi,
data: new Map([['key', 'value']]),
buffer: new ArrayBuffer(8),
nested: { set: new Set([1, 2, 3]) },
};
// Deep clone preserving all types
const clone = structuredClone(original);
clone.date instanceof Date; // true
clone.pattern instanceof RegExp; // true
clone.data instanceof Map; // true
clone.nested.set instanceof Set; // true
clone.date !== original.date; // true (different reference)With Transfer
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
// Clone the object but transfer the buffer (zero-copy)
const clone = structuredClone({ data: buffer }, { transfer: [buffer] });
buffer.byteLength; // 0 — ownership transferred
clone.data.byteLength; // 1048576structuredClone vs JSON vs Spread
| Feature | structuredClone | JSON.parse(JSON.stringify()) | Spread / Object.assign |
|---|---|---|---|
| Deep clone | Yes | Yes | No (shallow) |
| Date | Preserved | Converted to string | Shared reference |
| Map / Set | Preserved | Lost | Shared reference |
| RegExp | Preserved | Lost | Shared reference |
| ArrayBuffer | Preserved | Lost | Shared reference |
| Circular references | Handled | Throws | Shared reference |
| undefined | Preserved | Omitted | Preserved |
| Functions | Not supported | Not supported | Shared reference |
| DOM nodes | Not supported | Not supported | Shared reference |
| Performance (large objects) | Fast | Slower (serialize/parse) | N/A (shallow) |
Summary
| Feature | Core Problem Solved | Typical Scenario |
|---|---|---|
| WeakRef | Memory leaks from cached references | Object caches, DOM metadata |
| FinalizationRegistry | Resource cleanup after GC | Release native handles, update maps |
| AbortController | No cancellation mechanism | Fetch, event listeners, async flows |
| Symbols | Need unique, non-colliding keys | Private properties, protocol customization |
| Iterators | No lazy, on-demand value production | Pagination, large data, streams |
| Generators | Complex iteration logic | State machines, infinite sequences |
| BroadcastChannel | Cross-tab communication | Logout sync, theme sync, cache invalidation |
| structuredClone | Deep cloning with type preservation | Clone complex objects, worker messaging |
Best Practices
- Do not rely on
FinalizationRegistryfor critical cleanup — GC timing is non-deterministic - Always check
signal.abortedin long-running operations and listen for theabortevent - Use
Symbol.for()only for intentionally shared symbols — preferSymbol()for private keys - Generators should be used for lazy evaluation, not as a general replacement for functions
- Close
BroadcastChannelwhen no longer needed to prevent memory leaks - Prefer
structuredCloneoverJSON.parse(JSON.stringify())for deep cloning