Back to All Posts

You’ve Got Options for Removing Event Listeners

Reviewing some of the most common approaches available to remove event listeners in JavaScript.

Cleaning up your code in runtime is a non-negotiable part of building efficient, predictable applications. One of the ways that’s done in JavaScript is by stewarding event listeners well — specifically, removing them when they’re no longer needed.

There are several approaches available to do this, each with its own set of trade-offs that make it more appropriate in certain circumstances. We’re gonna run through a few of the most often used tactics, as well as a few considerations to keep in mind when you’re trying to decide which is best for the job at any given time.

We’ll be tinkering with the following setup — a button with a single click event listener attached:

<button id="button">Do Something</button>

<script>
document.getElementById('button').addEventListener('click', () => {
	console.log('clicked!');
});
</script>

Using Chrome’s getEventListeners() function, you’d see just one listener attached to that element:

In the event that you’d need to remove that listener, here’s what you might reach for.

Using .removeEventListener()

It’s probably the most obvious, but also the one with the most potential to threaten your sanity. The .removeEventListener() method accepts three parameters: the type of listener to remove, the callback function for that listener, and an options object.

But herein lies the (potentially) tricky part: those exact parameters must exactly match what were used when then the listener was set up, including the same reference in to the callback in memory. Otherwise, .removeEventListener() does nothing.

With that in mind, this would be totally ineffective:

document.getElementById('button').addEventListener('click', () => {
	console.log('clicked!');
});

document.getElementById('button').removeEventListener('click', () => {
	console.log('clicked!');
});

Even though that callback looks identical to the one originally attached, it’s not the same reference. The solution to this is to set the callback to a variable and reference it in both .addEventListener() and .removeEventListener().

const myCallback = () => {
  console.log('clicked!');
};

document.getElementById('button').addEventListener('click', myCallback);
document.getElementById('button').removeEventListener('click', myCallback);

Or, for particular use cases, you could also remove the listener by referencing a pseudo-anonymous function from within the function itself:

document
  .getElementById('button')
  .addEventListener('click', function myCallback() {
    console.log('clicked!');

    this.removeEventListener('click', myCallback);
  });

Despite its particularity, .removeEventListener() has the advantages of being very explicit in its purpose. There’s zero doubt as to what it’s doing when you’re reading through the code.

Using .addEventListener()’s once Option

The .addEventListener() method comes with a tool to help clean itself up if it’s intended for a one-time use: the once option. It’s about as simple as it sounds. If it’s set to true, the listener will automatically remove itself after first being invoked:

const button = document.getElementById('button');

button.addEventListener('click', () => {
	console.log('clicked!');
}, { once: true });

// 'clicked!'
button.click();

// No more listeners!
getEventListeners(button) // {} 

Assuming it fits your use case, this approach might be appropriate if you’re keen on using an anonymous function, given that your listener only needs to be invoked once.

Cloning & Replacing the Node

Sometimes, you don’t know about all the listeners active on a given node, but you do know you want to nuke them. In that case, it’s possible to clone the entire node and replace itself with that clone. Using the .cloneNode() method, none of the listeners attached via .addEventListener() will be carried over, giving it a clean slate.

Back in the stone age of client-side JavaScript, you would’ve seen this done by querying up to the parent, and replacing a particular child node with a clone:

button.parentNode.replaceChild(button.cloneNode(true), button);

But in modern browsers, that can be simplified with .replaceWith():

button.replaceWith(button.cloneNode(true));

The one thing that could potentially trip you up here is that intrinsic listeners are preserved, meaning a button with an onclick attribute would still fire as defined:

<button id="button" onclick="console.log('clicked!')">
	Do Something
</button>

In all, it’s an option worth reaching for if you need to indiscriminately remove listeners of any sort with brute force. On the downside, however, it suffers from being less obvious about its purpose. Some might go so far as to call it a hack.

Using AbortController()

This one’s new to me. I literally just read about it when I came across this tweet from Caleb Porzio. If you’re like me, you might’ve only heard of an AbortController being used to cancel fetch() requests. But it’s apparently more flexible than that.

As of recently, .addEventListener() can be configured with a signal for imperatively aborting/removing a listener. When the respective controller calls .abort(), that signal will trigger the listener to be removed:

const button = document.getElementById('button');
const controller = new AbortController();
const { signal } = controller;

button.addEventListener('click', () => console.log('clicked!'), { signal });

// Remove the listener!
controller.abort();

The most apparent advantage to this might be the ergonomics. It’s (in my opinion) a much clearer way of removing a listener without the potential gotcha of dealing with .removeEventListener(). But there’s a more tactical advantage too: you can use one signal to remove multiple listeners, of any sort, all at once. And using anonymous functions is totally fine too:

const button = document.getElementById('button');
const controller = new AbortController();
const { signal } = controller;

button.addEventListener('click', () => console.log('clicked!'), { signal });
window.addEventListener('resize', () => console.log('resized!'), { signal });
document.addEventListener('keyup', () => console.log('pressed!'), { signal });

// Remove all listeners at once:
controller.abort();

The only cause for hesitancy I came across is browser support. It’s a relatively new feature, with full support existing in Chrome only since 2021 (v90). So, keep this in mind if you need to support browser versions beyond only a couple years old.

Which Should I Choose?

As with everything else, “it depends.” Still, here’s how I might choose one over the other at a moment’s notice:

  • Use .removeEventListener() if the callback function is assigned to a variable and within easy reach of where the listener was added.
  • Use the once option in .addEventListener() if you need to fire a callback only once (duh).
  • Use the clone & replace approach if you need to indiscriminately nuke multiple listeners in one fell swoop.
  • Use AbortController() if you have a series of listeners you’d like to imperatively remove at once, or if you just like the syntax.

What’s Missing?

It’s very possible I’m missing another option in addition to these. If you happen to have one, feel free to drop a comment or let me know by some other means. At the very least, I hope this helps to mentally organize some of the the several paths available to clean up event listeners, and to also help prep your brain for the next time you need to manage them in your code.


Alex MacArthur is a software engineer working for Dave Ramsey in Nashville-ish, TN.
Soli Deo gloria.

Get irregular emails about new posts or projects.

No spam. Unsubscribe whenever.
Leave a Free Comment

15 comments
  • Graham Gough

    Can you help me out here please? I;v tried several things without success! How do I remove an event listener set up as follows? document.getElementById("upDV"+bookId+"A"+bookAv).addEventListener("click", function () {singleReset(bookId,bookAv);});
    Thank you in advance!


  • Yuki Zain

    Alex by this article I learn if you can't remove the event listener, then duplicate the node. That's all. Thanks!


  • Cava

    Alex, that {once:true} option saved my life, thanks!


    1 reply
  • Emry

    Interesting! I had no idea about this. Thanks for sharing!


  • Pierre

    Fascinating, I didn’t know AbortController can do that. We are using Web Components on our apps and we are heavily using removeEventListener. Having a single signal makes our codebase much smaller. Thanks Alex


  • Danny Engelman

    Since having a

  • Fagner Brack

    Consider using application code to avoid the function execution.

    If you always remove nodes that are not being used, it's a fair solution that moves the behavior of not executing something to your own domain models instead of relying on the native browser de-listening. The "no executing" path would follow in the next line with the side-effect of removing the offending node.

    Of course, it doesn't work if you want to keep the node there, only for elements that disappear.


  • Ruan Mendes

    I thought I wouldn't learn anything new but I didn't know about the once option or about using an AbortController to remove listeners. Both options seem a lot more ergonomic, especially the abort controller being able to unhook multiple (hopefully related) events.


  • Alex Rogers

    Enjoyed. Thanks for taking the time to share


    1 reply
  • David Hibshman

    How about how to remove inline element registrations that are not named functions but just a string of JS code to execute? Terrible implementation of course, but for completeness.

    Also not sure if this fits with your piece here but it might be worth noting that element.remove() and parent.removeChild(element) will also remove all attached event listeners so long as the element is reference free. If I recall correctly if the element is removed any children will also have their event listeners removed. Simlar to cloneNode and replaceWith but assuming you don't need the element any longer.


    1 reply
    • Alex

      Good question - for intrinsic/inline listeners, you can remove them by booting the attribute on the element. Ex:

      document.getElementById('button').removeAttribute('onclick');

      or..

      document.getElementById('button').onclick = null;


  • Vladimir Varbanov

    I a very nice one. Thank you, Alex. Haven't paid that attention to the cleaning part of the event listeners


    1 reply
  • dotnetcarpenter

    @Alex I’m old - I did client side in 2007 when IE6 was a thing. To answer your question: NO, it’s relatively cheap. But it depends on the element your are swapping. For this exact use case, jQuery introduced namespaced events. But it also have an off method. I think it is worth it to dig out that code in 2023 and see how it does it. Nuking an element will not be performant but in your use case,
    It might be milliseconds and not an issue.
    If you use React or similar,
    I’m pretty sure it will just be nuked


    1 reply
    • Alex

      Good thoughts - and wow, haven't thought/heard of jQuery's namespaced events until recently. You could probably build a modern version of that feature on top of native custom events.


  • Eduard Milushi

    AbortController its a good choice in the case its your code. The problem is always when the code is from a package written by someone else and you don’t want to overwrite the original code.


  • bubblez

    Had this problem a few times in the past ... but never read about AbortController. Seems the way to go :), thx!


    1 reply
  • Alex

    Hi, does replaceWith a clone costs in DOM operations?


    1 reply
    • Alex

      Yep, but I wouldn't be concerned about it until you're at considerable scale.