Unlocking JavaScript’s WeakMap & WeakSet: Memory‑Smart Patterns for Developers
This article explains the rarely understood ES6 features WeakMap and WeakSet, compares them to Java’s weak references, and demonstrates practical Node.js and front‑end use cases such as memory‑safe context storage, result caching, and tracking unhandled promise rejections.
ES6 has been popular for many years, but there are still some features that many developers are unfamiliar with, such as WeakMap and WeakSet. They feel unfamiliar because before ES6 there was no language feature that could achieve the same functionality, and because typical front‑end developers have limited understanding of memory management and garbage collection.
In some garbage‑collected languages, special references are implemented to allow different reclamation strategies. For example, Java implements SoftReference and WeakReference.
Soft reference: if an object is referenced only by a soft reference, it will be reclaimed when memory is low.
Weak reference: if an object is referenced only by a weak reference, it will be reclaimed whenever the garbage collector scans it, regardless of memory pressure. ES6 WeakMap and WeakSet belong to this weak‑reference case.
Map and Set usage is well known; below we mainly use WeakMap as an example to see what differs in the weak‑reference version.
In WeakMap, all traversal‑related functions or properties are removed, which means that after obtaining a WeakMap instance, if there is no key you cannot read any data. Another important characteristic is that its key must be an Object, so even if you know a key is an empty object, you cannot reconstruct an empty object instance to retrieve the data.
More detailed usage can be found in MDN, but we focus on what WeakMap is useful for. Below we show an example scenario.
const extInfoMap = new Map();
app.use(function middleWareA(ctx, next) {
extInfoMap.set(ctx, someValue);
return next();
});
function doSomeProcess(ctx, extInfoMap) {
const valueA = extInfoMap.get(ctx);
const valueB = doSomething(valueA);
extInfoMap.set(ctx, Object.assign(valueA, valueB));
}The above approach has a problem: because ctx should be destroyed after the request ends, using it as a key in extInfoMap prevents its reclamation, causing memory to grow until OOM. If we could determine the lifecycle of ctx, we might periodically clean the map, but without such knowledge we may resort to brute‑force timeouts.
Could WeakMap solve this? Yes, we can use WeakMap to add non‑intrusive properties to objects that may be destroyed, which is its most important capability.
let someObj = { myKey: 'myValue' };
const weakMap = new WeakMap();
weakMap.set(someObj, { hiddenKey, hiddenValue: 'hiddenValue' });
// later
weakMap.get(someObj); // -> { hiddenKey, 'hiddenValue' }
// when the original key object is no longer strongly referenced
someObj = null; // or replace with a new object
weakMap.has(someObj); // -> false
// after a GC cycle the entry disappears automatically
global.gc();Using this basic feature, other scenarios can be derived:
Heavy function result caching
Using WeakMap for caching means the cache lives only for the duration of a request or task, with the context object as the key, and the data is automatically released when the context is destroyed.
// Middleware to fetch full user info
app.use(async function (ctx, next) {
ctx.userInfo = await getUserExtInfo(ctx.cookies.get('SESSION'));
// some code ...
});
const userMap = new WeakMap();
async function getUserInfo() {
let info = userMap.get(this);
if (info === undefined) {
info = await getUserExtInfo(this.cookies.get('SESSION'));
userMap.set(this, info); // ctx destroyed → info auto‑destroyed
}
return info;
}
app.use(async function (ctx, next) {
ctx.getUserInfo = getUserInfo.bind(ctx);
// some code ...
});
async function someTask(ctx) {
ctx.getUserInfo(); // first trigger initiates query
}
async function anotherTask(ctx) {
ctx.getUserInfo(); // subsequent calls retrieve from userMap
}Adding tracking or instrumentation to dynamically created objects
The typical example is Node’s process.on('unhandledRejection', listener) implementation.
// Track all unhandled promise rejections
const maybeUnhandledPromises = new WeakMap();
function unhandledRejection(promise, reason) {
maybeUnhandledPromises.set(promise, { reason, uid: ++lastPromiseId, warned: false });
// some code ...
}
function handledRejection(promise) {
const promiseInfo = maybeUnhandledPromises.get(promise);
if (promiseInfo !== undefined) {
maybeUnhandledPromises.delete(promise);
// some code ...
}
}
function processPromiseRejections() {
const promise = pendingUnhandledRejections.shift();
const promiseInfo = maybeUnhandledPromises.get(promise);
if (promiseInfo === undefined) return;
// some code ...
process.emit('unhandledRejection', reason, promise);
// some code ...
}https://github.com/nodejs/node/blob/v12.x/lib/internal/process/promises.js#L24
If you are interested, you can search the Node source for WeakMap or WeakSet usage to discover more ideas.
Node Underground
No language is immortal—Node.js isn’t either—but thoughtful reflection is priceless. This underground community for Node.js enthusiasts was started by Taobao’s Front‑End Team (FED) to share our original insights and viewpoints from working with Node.js. Follow us. BTW, we’re hiring.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
