different-ai/openwork · error · InterpreterRuntimeError

for...of supports one declared binding.

Error message

for...of supports one declared binding.

What it means

The for...of head may declare exactly one binding per iteration element. If the left side is a VariableDeclaration with more than one declarator (e.g. `for (const a = 1, b = 2 of ...)`), the interpreter throws at the declaration node. Real JS cannot express multiple declarators in a for-of head either; this enforces that and the interpreter's simpler binding model.

Source

Thrown at packages/codemode/src/interpreter/runtime.ts:1098

    return Effect.gen(function* () {
      const left = getNode(node, "left")
      const right = yield* self.evaluateExpression(getNode(node, "right"))
      const body = getNode(node, "body")

      // Arrays iterate in place; strings iterate code points; Maps iterate [key, value]
      // pairs and Sets iterate values over a snapshot (mutation during iteration is safe).
      const iterable = Array.isArray(right) ? right : spreadItems(right)
      if (iterable === undefined) {
        throw new InterpreterRuntimeError("for...of requires an array, string, Map, or Set value in CodeMode.", node)
      }

      let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined
      let assignmentName: string | undefined

      if (left.type === "VariableDeclaration") {
        const declarations = getArray(left, "declarations")
        if (declarations.length !== 1) {
          throw new InterpreterRuntimeError("for...of supports one declared binding.", left)
        }

        const declarator = asNode(declarations[0], "declarations[0]")
        declaration = { pattern: getNode(declarator, "id"), mutable: getString(left, "kind") !== "const" }
      } else if (left.type === "Identifier") {
        assignmentName = getString(left, "name")
      } else {
        throw new InterpreterRuntimeError("Unsupported for...of binding.", left)
      }

      for (const value of iterable) {
        if (declaration) {
          self.pushScope()
          yield* self.declarePattern(declaration.pattern, value, declaration.mutable, left)
        } else if (assignmentName) {
          self.setIdentifierValue(assignmentName, value, left)
        }

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Use exactly one declarator: `for (const x of iterable)`.
  2. Destructure in the single binding if you need multiple values: `for (const [a, b] of pairs)`.
  3. Move extra declarations outside the loop.

Example fix

// before (AST with two declarators)
for (const a, b of items) ...
// after
for (const [a, b] of items) ...
Defensive patterns

Strategy: validation

Validate before calling

// AST check before execution
if (forOfNode.left.type === "VariableDeclaration" && forOfNode.left.declarations.length !== 1) {
  throw new Error("for...of head must declare exactly one binding");
}

Try / catch

try {
  interpret(src);
} catch (e) {
  if (e instanceof InterpreterRuntimeError && e.message.includes("one declared binding")) {
    // fix the AST/code to a single declarator or destructuring
  }
  throw e;
}

Prevention

When it happens

Trigger: A for...of whose left side is a VariableDeclaration whose `declarations` array length !== 1, typically from malformed or hand-written AST-like code.

Common situations: Programmatically generated CodeMode ASTs (e.g. by an LLM emitting raw nodes) that accidentally include two declarators; minified/transformed code that merged declarations.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/1c64631e7e6d56e6. Report an issue: GitHub.