oxc-project/oxc · error · OxcDiagnostic

Invalid `removeEventListener` call.

Error message

Invalid `removeEventListener` call.

What it means

Diagnostic from the oxlint rule `unicorn/no-invalid-remove-event-listener` (category: correctness). `removeEventListener(type, listener)` only removes the listener whose exact reference was passed to `addEventListener`. Passing a freshly created function — an inline arrow/function expression or the result of a `.bind()` call — creates a brand-new reference that matches nothing, so the removal is a silent no-op and the listener leaks. The rule flags exactly these shapes; a stored reference such as `handler` or `obj.method` passes.

Solutions

  1. Store the listener once and pass the same reference to add and remove: `const onClick = () => save(); el.addEventListener('click', onClick); el.removeEventListener('click', onClick);`
  2. For methods needing `this`, bind once (constructor or class field: `this.onClick = this.onClick.bind(this)`) and use `this.onClick` for both calls
  3. Prefer an AbortController: `const ac = new AbortController(); el.addEventListener('click', fn, { signal: ac.signal }); ac.abort();` removes everything

Example fix

// before
el.addEventListener('click', () => save());
el.removeEventListener('click', () => save()); // never removes anything

// after
const onClick = () => save();
el.addEventListener('click', onClick);
el.removeEventListener('click', onClick);
Defensive patterns

Strategy: validation

Validate before calling

// keep add/remove symmetrical through one helper
const registered = new WeakMap();
function on(target, type, fn, options) {
  target.addEventListener(type, fn, options);
  const set = registered.get(target) ?? new Set();
  set.add([type, fn, options]);
  registered.set(target, set);
}
function offAll(target) {
  for (const [type, fn, options] of registered.get(target) ?? []) {
    target.removeEventListener(type, fn, options);
  }
  registered.delete(target);
}

Prevention

When it happens

Trigger: `el.removeEventListener('click', () => handle())`, `window.removeEventListener('scroll', this.onScroll.bind(this))`, `el.removeEventListener('keydown', function (e) {})` — the second argument is a function expression, arrow function, or `anything.bind(...)` call.

Common situations: Component teardown where handlers were originally added inline; class code binding methods at removal time instead of storing the bound reference at add time; cleanup paths written far from the registration code.

Related errors


AI-assisted analysis of oxc-project/oxc@e1e7af627c (2026-08-20). Data as JSON: /api/errors/bedb6cf1a539e00f. Report an issue: GitHub.

Appendix: source

Thrown at crates/oxc_linter/src/rules/unicorn/no_invalid_remove_event_listener.rs:12

use oxc_ast::{
    AstKind,
    ast::{Argument, MemberExpression},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::{GetSpan, Span};

use crate::{AstNode, context::LintContext, rule::Rule};

fn no_invalid_remove_event_listener_diagnostic(call_span: Span, arg_span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("Invalid `removeEventListener` call.")
        .with_help("The listener argument should be a function reference.")
        .with_labels([
            call_span.label("`removeEventListener` called here."),
            arg_span.label("Invalid argument here"),
        ])
}

#[derive(Debug, Default, Clone)]
pub struct NoInvalidRemoveEventListener;

declare_oxc_lint!(
    /// ### What it does
    ///
    /// It warns when you use a non-function value as the second argument of `removeEventListener`.
    ///
    /// ### Why is this bad?
    ///
    /// The [`removeEventListener`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener) function must be called with a reference to the same function that was passed to [`addEventListener`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener). Calling `removeEventListener` with an inline function or the result of an inline `.bind()` call is indicative of an error, and won't actually remove the listener.

View on GitHub (pinned to e1e7af627c)