JavaScript permits “expando” properties such as element.myState on DOM objects. That is not automatically a security flaw, but it can collide with browser APIs or other scripts. Use an explicit storage mechanism instead.
Last updated: October 17, 2026.
const stateByElement = new WeakMap();
const button = document.querySelector('#saveButton');
button.dataset.action = 'save';
stateByElement.set(button, {
pending: false,
run: () => saveDocument()
});
button.addEventListener('click', () => {
const state = stateByElement.get(button);
if (!state.pending) state.run();
});The data-action value is visible in HTML and always stored as a string. The object and callback stay outside the element in a WeakMap.
Choose storage based on visibility and type
Use dataset for short values that belong in markup, such as an action name or record identifier. Writes become data-* attributes and values are converted to strings. Treat them as untrusted input when they can be changed in developer tools or generated from user content.
Use a WeakMap for objects, functions, request state, and implementation details. The DOM element becomes the key, and the mapping does not prevent the element from being garbage-collected. Unlike a normal property, the state is not exposed by enumerating the element.
Understand the actual security boundary
Storing a function does not execute it; the risk appears when code later invokes a value from an untrusted source or writes untrusted text into an unsafe HTML sink. Never convert a string from dataset into code with eval() or new Function(). Map approved action names to functions you define.
Direct custom properties may be acceptable inside a small, fully controlled component, but use a Symbol to reduce naming collisions. Do not overwrite names such as onclick, remove, or browser-defined methods. Continue with running code after the DOM is ready, updating attribute-driven CSS, and tracing JavaScript failures.