
Utilizing WeakMap to effectively store Metadata file
Gemini said Mastering Metadata in JavaScript: Harnessing WeakMaps for Efficient, Memory-Safe Object Extensions Without Manual Cleanup or Memory Leaks. Learn how to utilize WeakMaps to attach private metadata to objects, ensuring that data is automatically garbage-collected when the object is no longer in use, thus maintaining optimal application performance and cleaner code.
Utilizing WeakMap to effectively store Metadata file
Metadata is simply data that holds information about some other piece of data. Seems repetitive but if you were to think of networking and mainly HTTP. If you perform an HTTP request, data is sent or received (HTTP response) together with some HTTP headers. The HTTP headers are considered metadata and specifically descriptive metadata. So as much as you as the recipient of the request might want to read the data sent, you will need the HTTP headers to be able to understand the data you have been sent.
This caught my interest because most of the time, when I am creating a web application. I would want my, let say DOM elements, to hold some information (metadata). I don't want to mutate them by adding attributes so the most obvious way I thought of, is to use a cache object. An example of a cache object would be
const cache = {};
cache["user1"] = {
object: new User("admin"),
meta: {
syncedOn: new Date("2009-11-27T16:45:30"),
}
};
cache["user2"] = {
object: new User("student"),
meta: {
syncedOn: new Date("2009-11-27T09:45:30"),
}
};
Some problems with the example above are:
- Iteration can be a pain
for (const key in cache) {
// check all users whose last sync were 20 minutes ago
}
// OR
Object.keys(cache).map(someComputationBasedOnMetadata);
- If user2 is no longer used it won't be garbage collected since there is still a lingering reference to it. This causes memory leaks.
Solving iteration in JavaScript
In JavaScript, we can make an object to behave like an Array by adding Symbol.iterator generator function which yield the values required
const cache = {
*[Symbol.iterator]() {
for (const k of Object.keys(this)) {
// you could do more computation
yield this[k];
}
}
};
cache["john_doe"] = {
object: new User("admin"),
meta: {
syncedOn: new Date("2024-05-07T00:45:30")
}
};
cache["jane_doe"] = {
object: new User("root"),
meta: {
syncedOn: new Date("2024-05-07T03:15:30")
}
};
for (const key of Object.keys(cache)) {
const metadata = cache[key];
// some computation done here!
}
Managing garbage collection
Here is where a tradeoff comes in. We are going to introduce a JavaScript data structure WeakMap.
A WeakMap is a collection of key/value pairs where the key is an object or non-registered symbol and the values are normal (or arbitrary) JavaScript types e.g. Boolean or Number or JavaScript objects ().
Although it has a similar API to a Map it has one main advantage which is the keys provided are never restricted from being garbage collected. This means:
const wm = new WeakMap();
const signupForm = document.querySelector("form[data-purpose='signup']");
wm.set(
signupForm,
{
lastFilledOn: new Date("2024-05-09T13:05:00")
}
);
In an event where signupForm is no longer referenced, it will be garbage collected. This means later on wm.has(signupForm) is returns false.
Now the tradeoff is we cannot iterate over a WeakMap. The reason is because unlike Maps, WeakMaps do not keep an array or list of keys since its keys are non-deterministic or garbage collectable.
WeakMap use case
Going back to our cache example, we can expand it by assuming a real-world use case.
Let's say we have a table of company employees. We are tasked to improve the table's functionality by enabling in-memory editing. We can cache the state of our form per row using WeakMap following the pattern.
type EditFormCache = WeakMap<HTMLFormElement, "editing" | "stalled">;
This way when we toggle back and forth between a normal table row and a form row, we can only get forms which have not yet been garbage collected stored in our cache.
const wm = new WeakMap();
const form = document.querySelectorAll("form.table-row-edit-form");
for (const f of form) {
wm.set(f, "stalled");
}
Then when the user is typing in an input you can capture the parent (HTMLFormElement) and update the state
const form = document.querySelector("input.my-input").parentElement;
if (wm.has(form)) {
const state = wm.get(form);
switch (state) {
case "editing": {
wm.set(form, "stalled");
break;
}
case "stalled": {
wm.set(form, "editing");
break;
}
}
}
Then maybe after updating the state we can use WebSocket API to save the data entered if state is "stalled" or even inform other clients of the current state of the table row i.e. "editing".
Closing remarks
The example above may resemble a real-world scenario but the truth of the matter is there is more work to it than meets the eye. That being said, you will notice that,
- If your table is paginated then your cache will always stay fresh since it will remove the garbage collected keys.
- The operations used by WeakMap are fast since it doesn't encourage loops hence most operations are of O(1).
- It is extensible allowing you to create robust collections like ClearableWeakMap and you can even use them in OOP factories when managing produced objects.
All in all, I found it to be an interesting JavaScript collection. I am planning to see how I can create an Inversion of Control (IoC) library with it. I would love to hear the cool ways you have or would want to use WeakMap.
Thank you and Happy coding!
Related Reads

Utilizing WeakMap to effectively store Metadata file
Gemini said Mastering Metadata in JavaScript: Harnessing WeakMaps for Efficient, Memory-Safe Object Extensions Without Manual Cleanup or Memory Leaks. Learn how to utilize WeakMaps to attach private metadata to objects, ensuring that data is automatically garbage-collected when the object is no longer in use, thus maintaining optimal application performance and cleaner code.

When in an error, I defer to Golang than Typescript (node.js implied 😉)
An argument that Go’s explicit error handling and defer statement offer superior reliability compared to the implicit try/catch patterns in TypeScript/Node.js. While Go’s if err != nil approach is more verbose, it treats errors as first-class values, forcing developers to handle failures immediately. This reduces mental overhead' and prevents the silent failures or unhandled promises common in complex, asynchronous Node.js environments.