#18200·babel

transform-block-scoping: closures share a binding when a nested for-loop head has an initializer

Author: ko-successCreated Aug 24, 2026Updated Sep 11, 2026

  • Would you like to work on a fix?

How are you using Babel?

babel-loader (webpack)

Input code

REPL reproduction

javascript
var arr = {};

for (var i = 0; i < 2; i++)
  for (let x = (arr[i] = () => x, i); false;);

console.log(arr[0](), arr[1]()); // 0 1

Configuration file name

No response

Configuration

json
{
  "plugins": ["@babel/plugin-transform-block-scoping"]
}

Current and expected behavior

javascript
var arr = {};

for (var i = 0; i < 2; i++)
  for (var x = (arr[i] = () => x, i); false;);

console.log(arr[0](), arr[1]()); // 1 1

Environment

  • Babel version: 8.0.4
  • @babel/plugin-transform-block-scoping: 8.0.1
  • Node: 24.14.0
  • npm: 11.11.0
  • OS: macOS
  • Monorepo: no

Possible solution

No response

Additional context

This is related to #18088, but it exposes a separate part of lexical loop semantics that cannot be preserved by reinitializing the lowered var.

#18088 correctly handles nested loop-head declarations without an initializer. For example:

javascript
for (var i = 0; i < 2; i++) {
  for (let done; !done; done = true) {
    // ...
  }
}

When let done; is lowered to var done;, re-entering the inner loop does not perform an assignment, so done retains the value from the previous execution. Transforming it to var done = void 0; correctly restores the required value initialization on every execution of the inner loop.

However, lexical declarations provide more than value initialization: every evaluation of the declaration also creates a new binding in a new lexical environment. That distinction becomes observable when the binding is captured by a closure.