oxc-project/oxc · warning · OxcDiagnostic

Function declared in a loop contains unsafe references to va

Error message

Function declared in a loop contains unsafe references to variable(s)

What it means

Diagnostic from oxlint's eslint/no-loop-func rule (crates/oxc_linter/src/rules/eslint/no_loop_func.rs:20). It reports a function, arrow function, or class method declared inside a loop that references variables from an unsafe outer scope — typically 'var' declarations in the same loop or an enclosing one. Because var is function-scoped, every closure created in the loop shares one binding, producing the classic 'all callbacks see the last value' bug.

Source

Thrown at crates/oxc_linter/src/rules/eslint/no_loop_func.rs:20

use oxc_ast::{
    AstKind,
    ast::{
        ArrowFunctionExpression, DoWhileStatement, ForInStatement, ForOfStatement, ForStatement,
        Function, IdentifierReference, Statement, WhileStatement,
    },
};
use oxc_ast_visit::{VisitJs, walk_js};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_semantic::{AstNode, NodeId, ScopeId, SymbolId};
use oxc_span::{GetSpan, Span};
use oxc_syntax::{scope::ScopeFlags, symbol::SymbolFlags};

use crate::{ast_util::outermost_paren_parent, context::LintContext, rule::Rule};

fn no_loop_func_diagnostic(span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("Function declared in a loop contains unsafe references to variable(s)")
        .with_help("Variables declared with 'var' are function-scoped, not block-scoped. Consider using 'let' or 'const' for block-scoped variables, or move the function outside the loop.")
        .with_label(span)
}

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

declare_oxc_lint!(
    /// ### What it does
    ///
    /// Disallows function declarations and expressions inside loop statements
    /// when they reference variables declared in the outer scope that may change
    /// across iterations.
    ///
    /// ### Why is this bad?
    ///
    /// Writing functions within loops tends to result in errors due to the way
    /// closures work in JavaScript. Functions capture variables by reference,

View on GitHub (pinned to e1e7af627c)

Solutions

  1. Change the loop variable (and any loop-body vars the function reads) from var to let so each iteration gets its own binding
  2. Move the function outside the loop and pass the changing value as a parameter
  3. For legacy targets, wrap in an IIFE: (function (i) { ... })(i), or use Array.prototype.map/forEach which bind per element

Example fix

// before
var handlers = [];
for (var i = 0; i < 3; i++) {
  handlers.push(function () { return i; }); // all return 3
}

// after
const handlers = [];
for (let i = 0; i < 3; i++) {
  handlers.push(function () { return i; }); // 0, 1, 2
}
Defensive patterns

Strategy: validation

Validate before calling

// Codegen-time guard: refuse to emit closures over loop-scoped var
function assertLoopSafe(declKind, captured) {
  if (declKind === 'var' && captured) {
    throw new Error('closure captures var declared in a loop; use let');
  }
}

Type guard

function isSafeLoopCapture(decl) {
  return decl.kind === 'let' || decl.kind === 'const';
}

Prevention

When it happens

Trigger: for (var i = 0; i < 3; i++) { setTimeout(function () { console.log(i); }); }; forEach-style loops pushing callbacks that read a var declared inside the loop body; functions inside loops referencing an outer var that the loop reassigns.

Common situations: Building arrays of handlers in a loop; pre-ES2015 codebases where let was unavailable; converting such code to oxlint from ESLint and hitting the same rule; interview-style closure bugs surfacing in production.

Related errors


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