Why removeeventlistener() Needs the Same Function Reference

Image by Zoshua Colah.

In Brief

Keep a reference to the exact function passed to addEventListener() and pass that reference to removeEventListener(), together with the same event type and capture setting. Repeating an anonymous function creates a different function object. Internet Explorer 8 and earlier use the samereference rule with attachEvent() and detachEvent().

Adding an event listener with an anonymous function is convenient. Removing that listener later can be confusing, because writing the same anonymous function again does not identify the function which was registered.

The two pieces of source code may look identical, but JavaScript creates a new function object each time it evaluates a function expression. The event target remembers the first object, so handing it the second one does not produce a match.


Repeating a Function Does Not Reuse It

This listener will continue to run after the attempted removal:

var button = document.getElementById('save');button.addEventListener('click', function () {  alert('Saved');}, false);button.removeEventListener('click', function () {  alert('Saved');}, false);

The first function expression creates one object and registers it. The second expression creates another object which happens to contain the same statements. removeEventListener() compares the listener reference, not the function's source text, so it finds nothing to remove.

This is the same distinction we see with ordinary objects:

var first = {};var second = {};alert(first === second);  // false

The objects have the same shape, but they do not have the same identity. Functions are objects too, and event registration depends on that identity.


Keep the Handler in a Variable or Declaration

The straightforward solution is to give the handler a stable reference:

var button = document.getElementById('save');function handleSaveClick() {  alert('Saved');}button.addEventListener('click', handleSaveClick, false);// Later, when the button should stop responding:button.removeEventListener('click', handleSaveClick, false);

Both calls receive handleSaveClick, which refers to the same function object. Assigning a function expression to a variable works in the same way, provided that variable remains available when cleanup happens.

There is no requirement for every handler to become a global function. A setup function can keep the handler in its own scope and return the corresponding cleanup operation:

function initialiseSaveButton(button) {  function handleSaveClick() {    alert('Saved');  }  button.addEventListener('click', handleSaveClick, false);  return function destroy() {    button.removeEventListener('click', handleSaveClick, false);  };}var destroySaveButton = initialiseSaveButton(  document.getElementById('save'));// Call this before discarding or reinitialising the control.destroySaveButton();

The returned destroy function retains access to the original handler. This makes the lifecycle of a small widget clear without exposing its implementation to the rest of the page.


The Capture Setting is Part of the Match

The DOM Level 2 Events specification defines a listener registration by its event type, listener, and useCapture value. removeEventListener() therefore needs the same event type, listener reference, and useCapture value that identify the registration.

This pair does not match:

button.addEventListener('click', handleSaveClick, true);button.removeEventListener('click', handleSaveClick, false);

The first call registers a capturing listener. The second call asks to remove a noncapturing listener with the same function. If no such registration exists, the removal has no effect.

Using false explicitly in both places makes the intent obvious, particularly in older code where browser support and examples vary.


Internet Explorer Uses Different Method Names

There is an important browser qualification in 2010. Internet Explorer 8 and earlier do not implement the DOM Level 2 addEventListener() and removeEventListener() methods. They use Microsoft's attachEvent() and detachEvent() instead, with event names such as onclick.

The same function reference is still required. A small compatibility pair can preserve it:

function addListener(element, type, listener) {  if (element.addEventListener) {    element.addEventListener(type, listener, false);  } else if (element.attachEvent) {    element.attachEvent('on' + type, listener);  }}function removeListener(element, type, listener) {  if (element.removeEventListener) {    element.removeEventListener(type, listener, false);  } else if (element.detachEvent) {    element.detachEvent('on' + type, listener);  }}function handleSaveClick() {  alert('Saved');}var button = document.getElementById('save');addListener(button, 'click', handleSaveClick);removeListener(button, 'click', handleSaveClick);

attachEvent() has other differences, including its handling of this and event order, but they do not change the identity rule. The archived Microsoft documentation for detachEvent() likewise requires the function previously supplied to attachEvent().

A compatibility helper which creates a fresh wrapper during removal would reintroduce the original problem. If wrapping a listener is necessary, store the wrapper and remove that same wrapper later.


Handler Factories Need the Same Care

A function which returns a handler is useful when the callback needs to remember a value. It also creates a new function every time it is called:

function saveRecord(recordId) {  alert('Saved record ' + recordId);}function createSaveHandler(recordId) {  return function () {    saveRecord(recordId);  };}var saveHandler = createSaveHandler(42);addListener(button, 'click', saveHandler);removeListener(button, 'click', saveHandler);

Calling createSaveHandler(42) again during removal would not reproduce saveHandler. It would return another closure with the same record number and different identity. Store the result of the factory, just as we store any other event handler which will need to be removed.


Cleanup is Part of Initialisation

This matters most when controls are initialised more than once, fragments of a page are replaced, or a widget keeps references to data which is no longer needed. Failed cleanup can leave an old callback responding alongside a new one and can retain its surrounding variables for longer than intended.

It does not follow that every listener must be removed. A listener attached for the lifetime of the document can quite reasonably remain there. The useful rule is that code which owns a shorterlived interaction should keep enough information to undo what it registered. For an event listener, that information includes the original function reference.

Postscript

Aug 2026: This article forms part of an archive restored from a previous version of my website. Its original publication date is accurate. During the restoration, I reviewed and updated it where appropriate for formatting, imagery, broken links, code correctness, and current internal references, whilst preserving the original technical context and intent. Internet Explorer's attachEvent() model is now obsolete, and modern addEventListener() supports options including passive and signal. Direct removal still depends on the original callback and matching capture value. Improving Scroll Performance with Passive Event Listeners covers one of those later listener options.

Looking for technical direction?

I support teams that need senior judgement on React, Next.js, headless CMS architecture, performance, migrations, and technical SEO.