Leaderboard
Javascript May 7, 2026
Javascript Duplicate Event Listener Bug

JavaScript Event Listeners — Spot the Memory Leak Bug

This is a daily Javascript challenge from the CodeShot archive. Practice your knowledge of Duplicate Event Listener Bug and improve your technical interview readiness.

function addClickHandler() {
  document.getElementById("btn")
    .addEventListener("click", () => {
      console.log("clicked")
    })
}

// Called on every render
addClickHandler()
A addEventListener is spelled wrong
B Each call adds a new listener — the same button fires multiple times
C Arrow functions cannot be used in event listeners
D getElementById should be querySelector

Detailed Explanation

Why This Question Matters

If you've spent any time working with vanilla JavaScript or early versions of React/Vue, you've probably run into a weird bug where a single button click triggers an action five times. You check your code, you see one addEventListener call, and you're left scratching your head.

This is a classic memory leak and performance trap. The issue isn't that the code is "broken" in the sense that it crashes; it's that it behaves unpredictably. Understanding how the browser handles event listeners is the difference between a smooth app and one that feels sluggish or triggers duplicate API calls every time a user clicks a button.

Understanding the Code

Let's look at the snippet:

function addClickHandler() {
  document.getElementById("btn")
    .addEventListener("click", () => {
      console.log("clicked")
    })
}

// Called on every render
addClickHandler()

At first glance, it looks fine. You have a function that grabs a button and attaches a click listener. Simple.

The problem is the comment: // Called on every render.

In a real application—especially one using a component-based architecture—functions like this are often called inside a loop, a render cycle, or a state update. Every time addClickHandler() runs, the browser doesn't check if the listener already exists. It just says, "Cool, another function to run when this button is clicked," and adds it to the internal list of listeners for that element.

If this function runs 10 times, you now have 10 identical anonymous functions sitting in memory, all waiting to fire. When the user clicks the button once, you'll see "clicked" printed 10 times in the console.

Finding the Correct Answer

The core of the problem is that anonymous functions are unique.

When you pass an arrow function () => { ... } directly into addEventListener, you are creating a new function object in memory every single time that line of code executes.

Even if the code inside the function is identical, JavaScript treats these as different entities. Because the browser only ignores duplicate listeners if the function reference is exactly the same, these anonymous functions keep piling up.

To fix this, you have a few options:
1. Named Functions: Define the handler outside the render loop and pass the reference.
2. Removal: Call removeEventListener before adding a new one (though this requires a named function).
3. Once: Use the { once: true } option if the event should only happen once.
4. Logic Guard: Only call the attachment function once during the initial app mount.

Common Mistakes Developers Make

The most common mistake is assuming that addEventListener behaves like a property assignment (e.g., element.onclick = ...).

If you use element.onclick, you are overwriting the previous value. Only one function can be assigned to onclick. However, addEventListener is designed specifically to allow *multiple* listeners for the same event. While this is powerful, it's also where the "duplicate listener" bug lives.

Another trap is thinking that clearing the innerHTML of a parent element automatically cleans up all listeners. While modern browsers are pretty good at garbage collecting listeners when an element is removed from the DOM, relying on this blindly can lead to memory leaks in complex single-page applications (SPAs).

Real-World Usage

In a production environment, this usually happens in "Component Mount" logic.

Imagine you're building a custom dropdown. You add a listener to the window to detect clicks outside the dropdown so you can close it. If your component re-renders due to a state change and you call that "init" function again, you've just leaked another global event listener.

If a user spends an hour on your page and the component re-renders 50 times, you now have 50 listeners firing on every single window click. This will eventually tank the frame rate and make the UI feel janky.

The professional way to handle this is to store the function reference and clean it up:

const handleOutsideClick = (e) => {
  console.log("Closing dropdown...");
};

// When the component mounts/starts
window.addEventListener("click", handleOutsideClick);

// When the component unmounts/destroys
window.removeEventListener("click", handleOutsideClick);

Key Takeaways

  • addEventListener does not overwrite; it appends.
  • Anonymous functions (arrow functions) created inside a loop or render function are unique references.
  • If you attach a listener inside a function that runs multiple times, you are creating a memory leak.
  • Always use named functions if you ever intend to remove the listener later.
  • Be mindful of the lifecycle of your application—know exactly when a listener is added and when it is torn down.
  • Why this matters

    Understanding Duplicate Event Listener Bug is crucial for passing technical interviews. In real-world applications, this concept often leads to subtle bugs if not handled correctly. For more details, you can always refer to the official MDN Documentation.

    📝
    Reviewed by CodeShot Editorial
    Every challenge is code-reviewed by senior developers to ensure accuracy and real-world relevance. Learn more.

    Ready for your shot?

    Join thousands of developers solving one logic puzzle every morning.

    Solve Today's Challenge →