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
- Change the loop variable (and any loop-body vars the function reads) from var to let so each iteration gets its own binding
- Move the function outside the loop and pass the changing value as a parameter
- 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
- Ban var in new code (prefer-const / no-var rules) so loop closures are always safe
- Pass loop values as function parameters instead of closing over them
- Write a quick test that asserts each generated callback returns its own iteration value
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
- This number literal will lose precision at runtime.
- Empty array binding pattern
- Empty object binding pattern
- React Hook {hook_name:?} cannot be called inside a callback.
- `debugger` statement is not allowed
AI-assisted analysis of oxc-project/oxc@e1e7af627c (2026-08-20).
Data as JSON: /api/errors/6800ed80b8e14100.
Report an issue: GitHub.