{"record":{"id":"bedb6cf1a539e00f","repo":"oxc-project/oxc","slug":"invalid-removeeventlistener-call","errorCode":null,"errorMessage":"Invalid `removeEventListener` call.","messagePattern":"Invalid `removeEventListener` call\\.","errorType":"validation","errorClass":"OxcDiagnostic","httpStatus":null,"severity":"error","filePath":"crates/oxc_linter/src/rules/unicorn/no_invalid_remove_event_listener.rs","lineNumber":12,"sourceCode":"use oxc_ast::{\n    AstKind,\n    ast::{Argument, MemberExpression},\n};\nuse oxc_diagnostics::OxcDiagnostic;\nuse oxc_macros::declare_oxc_lint;\nuse oxc_span::{GetSpan, Span};\n\nuse crate::{AstNode, context::LintContext, rule::Rule};\n\nfn no_invalid_remove_event_listener_diagnostic(call_span: Span, arg_span: Span) -> OxcDiagnostic {\n    OxcDiagnostic::warn(\"Invalid `removeEventListener` call.\")\n        .with_help(\"The listener argument should be a function reference.\")\n        .with_labels([\n            call_span.label(\"`removeEventListener` called here.\"),\n            arg_span.label(\"Invalid argument here\"),\n        ])\n}\n\n#[derive(Debug, Default, Clone)]\npub struct NoInvalidRemoveEventListener;\n\ndeclare_oxc_lint!(\n    /// ### What it does\n    ///\n    /// It warns when you use a non-function value as the second argument of `removeEventListener`.\n    ///\n    /// ### Why is this bad?\n    ///\n    /// 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.","sourceCodeStart":1,"sourceCodeEnd":30,"githubUrl":"https://github.com/oxc-project/oxc/blob/e1e7af627c8843ab64044ed466b128fcc21a035b/crates/oxc_linter/src/rules/unicorn/no_invalid_remove_event_listener.rs#L1-L30","documentation":"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.","triggerScenarios":"`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.","commonSituations":"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.","solutions":["Store the listener once and pass the same reference to add and remove: `const onClick = () => save(); el.addEventListener('click', onClick); el.removeEventListener('click', onClick);`","For methods needing `this`, bind once (constructor or class field: `this.onClick = this.onClick.bind(this)`) and use `this.onClick` for both calls","Prefer an AbortController: `const ac = new AbortController(); el.addEventListener('click', fn, { signal: ac.signal }); ac.abort();` removes everything"],"exampleFix":"// before\nel.addEventListener('click', () => save());\nel.removeEventListener('click', () => save()); // never removes anything\n\n// after\nconst onClick = () => save();\nel.addEventListener('click', onClick);\nel.removeEventListener('click', onClick);","handlingStrategy":"validation","validationCode":"// keep add/remove symmetrical through one helper\nconst registered = new WeakMap();\nfunction on(target, type, fn, options) {\n  target.addEventListener(type, fn, options);\n  const set = registered.get(target) ?? new Set();\n  set.add([type, fn, options]);\n  registered.set(target, set);\n}\nfunction offAll(target) {\n  for (const [type, fn, options] of registered.get(target) ?? []) {\n    target.removeEventListener(type, fn, options);\n  }\n  registered.delete(target);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Register listeners only through named references or AbortController signals (`ac.abort()` removes them all)","Never pass an inline function or `.bind()` result to removeEventListener — new reference, silent no-op","Do teardown in the same effect/lifecycle scope that did setup"],"tags":["events","dom","memory-leak","bug","oxlint"],"backgroundTag":"event-listener-reference-mismatch","analyzedSha":"e1e7af627c8843ab64044ed466b128fcc21a035b","analyzedAt":"2026-08-20T07:01:07.079Z","contentChangedAt":"2026-08-20T07:01:07.079Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}