Why Event Listeners Leak Memory
Event listeners are everywhere in interactive JavaScript applications, but they can quietly cause memory leaks if left attached when no longer needed. The real problem is simple: listeners are often added and never removed. This is especially tricky when a handler needs parameters — a common situation with dynamic task lists where a Delete button must know its task ID.
In such cases, removing the listener after the task completes is important so that the deleted element can be garbage-collected properly.
The Classic Callback Mistake
A frequent error is invoking the function directly inside addEventListener():
button.addEventListener('click', myFunction(param1, param2));
The browser executes this immediately, regardless of whether the click has happened — so the function never fires on the actual event. In some browsers this also produces a console error:
addEventListener on EventTarget: parameter is not of type Object. (Large preview)That error occurs because the second parameter of addEventListener only accepts a function, an object with a handleEvent() method, or null. A straightforward fix is to wrap the call in an arrow or anonymous function:
button.addEventListener('click', (event) => {
myFunction(event, param1, param2); // Runs on click
});
The catch is that arrow and anonymous functions can’t be removed with the usual removeEventListener(). For one or two listeners, that method still works if you keep a reference to the handler — but with arrow functions you’ll need AbortController instead.
Adding Parameters to Handlers
Two practical approaches handle parameters cleanly: arrow/anonymous functions with AbortController, and closures with removeEventListener().
Method 1: Arrow and Anonymous Functions
This is the quickest route. First, call your function inside an arrow function attached to the listener:
const button = document.querySelector("#myButton");
button.addEventListener("click", (event) => {
handleClick(event, "hello", "world");
});
Then define the parameterized function:
function handleClick(event, param1, param2) {
console.log(param1, param2, event.type, event.target);
}
Removal requires AbortController. Create a controller and pull its signal:
const controller = new AbortController();
const { signal } = controller;
Pass the signal as an option to addEventListener:
button.addEventListener("click", (event) => {
handleClick(event, "hello", "world");
}, { signal });
Then abort to remove the listener:
controller.abort()
Method 2: Closures
Closures solve the type error differently by leveraging scope. An inner function can access variables from its enclosing function, meaning parameters can come from the outer scope:
function createHandler(message, number) {
// Event handler
return function (event) {
console.log(`${message} ${number} - Clicked element:`, event.target);
};
}
const button = document.querySelector("#myButton");
button.addEventListener("click", createHandler("Hello, world!", 1));
}
Here the outer function returns another function, which is what actually gets attached as the handler. The inner function receives the event object automatically because it’s the registered listener — while closure access gives it the parameters from outside.
For removal with removeEventListener(), store a reference to the creator function and use it when adding:
function createHandler(message, number) {
return function (event) {
console.log(`${message} ${number} - Clicked element:`, event.target);
};
}
const handler = createHandler("Hello, world!", 1);
button.addEventListener("click", handler);
Now the listener can be removed normally:
button.removeEventListener("click", handler);
Managing Listener Lifecycles
Always remove event listeners when they’re no longer needed to avoid memory leaks. Most handlers don’t need parameters, but when they do, closures, AbortController, and removeEventListener all provide reliable ways to both pass arguments and properly clean up.




