Skip to main content

no-unreachable-loop

A loop that can never reach its second iteration is usually a mistake. If you only need one iteration, use an if statement instead.
Rule Type: Problem
Fixable: No

Why This Rule Exists

Loops are meant to execute multiple times. If a loop always exits on the first iteration (via break, return, or throw), it should be refactored to use if statements instead.
// This loop can only run once
for (let i = 0; i < arr.length; i++) {
    if (arr[i].name === myName) {
        doSomething(arr[i]);
        // break was supposed to be here!
    }
    break; // Always exits after first iteration
}

Rule Details

This rule detects loops where all code paths exit the loop in the first iteration via:
  • break statement
  • return statement
  • throw statement
It checks these loop types:
  • while loops
  • do-while loops
  • for loops
  • for-in loops
  • for-of loops

Examples

Incorrect Code

// Unconditional break
while (foo) {
    doSomething(foo);
    foo = foo.parent;
    break; // Always exits after first iteration
}

// All branches return
function verifyList(head) {
    let item = head;
    do {
        if (verify(item)) {
            return true;
        } else {
            return false; // No matter what, we return
        }
    } while (item);
}

// All branches throw or return
function findSomething(arr) {
    for (let i = 0; i < arr.length; i++) {
        if (isSomething(arr[i])) {
            return arr[i];
        } else {
            throw new Error("Doesn't exist."); // Every path exits
        }
    }
}

// Break in all paths
for (const key in obj) {
    if (key.startsWith("_")) {
        break;
    }
    firstKey = key;
    firstValue = obj[key];
    break; // Break either way
}

// Immediate break
for (const foo of bar) {
    if (foo.id === id) {
        doSomething(foo);
    }
    break; // Always break immediately
}

Correct Code

// Loop that can actually iterate
while (foo) {
    doSomething(foo);
    foo = foo.parent; // No break - can continue
}

// Conditional return allows iteration
function verifyList(head) {
    let item = head;
    do {
        if (verify(item)) {
            item = item.next; // Continue to next item
        } else {
            return false; // Only exit on failure
        }
    } while (item);
    return true;
}

// Only returns when found
function findSomething(arr) {
    for (let i = 0; i < arr.length; i++) {
        if (isSomething(arr[i])) {
            return arr[i]; // Exit when found
        }
        // Keep looking
    }
    throw new Error("Doesn't exist."); // After checking all
}

// Continue instead of break
for (const key in obj) {
    if (key.startsWith("_")) {
        continue; // Skip this one, try next
    }
    firstKey = key;
    firstValue = obj[key];
    break; // Break after finding first valid key
}

// Conditional break
for (const foo of bar) {
    if (foo.id === id) {
        doSomething(foo);
        break; // Only break when found
    }
}

Common Refactorings

Replace Loop with If Statement

// Before: Loop that runs once
for (const item of items) {
    if (item.isValid) {
        process(item);
        return true;
    }
    return false;
}

// After: Just use if
const item = items[0];
if (item && item.isValid) {
    process(item);
    return true;
}
return false;

Use Array Methods

// Before: Loop that finds first match
for (const item of items) {
    if (item.id === targetId) {
        return item;
    }
    break; // Bug: should check all items
}

// After: Use find()
return items.find(item => item.id === targetId);

Fix the Break Statement

// Before: Misplaced break
for (let i = 0; i < arr.length; i++) {
    if (arr[i] === target) {
        found = true;
        // break should be here!
    }
    break; // Wrong: exits immediately
}

// After: Break in right place
for (let i = 0; i < arr.length; i++) {
    if (arr[i] === target) {
        found = true;
        break; // Exit when found
    }
}

Replace Break with Continue

// Before: Breaks after skipping one item
for (const item of items) {
    if (item.skip) {
        break; // Bug: should continue, not break
    }
    process(item);
}

// After: Continue to next item
for (const item of items) {
    if (item.skip) {
        continue; // Skip this one, check others
    }
    process(item);
}

Options

ignore

Type: Array<string> Specify loop types to ignore. Possible values:
  • "WhileStatement" - ignore while loops
  • "DoWhileStatement" - ignore do-while loops
  • "ForStatement" - ignore for loops
  • "ForInStatement" - ignore for-in loops
  • "ForOfStatement" - ignore for-of loops
// Ignore for-in and for-of loops
{
  "rules": {
    "no-unreachable-loop": ["error", { 
      "ignore": ["ForInStatement", "ForOfStatement"] 
    }]
  }
}
Example: Sometimes you intentionally want to check only the first property:
/* eslint no-unreachable-loop: ["error", { "ignore": ["ForInStatement"] }] */

// Intentionally check only first property
for (const key in obj) {
  hasEnumerableProperties = true;
  break; // OK with ignore option
}

Known Limitations

This rule uses static code path analysis and doesn’t evaluate conditions.
The rule may miss obvious cases:
// This is technically always unreachable, but rule doesn't evaluate `true`
for (let i = 0; i < 10; i++) {
    doSomething(i);
    if (true) { // Static analysis doesn't know this is always true
        break;
    }
}

When Not to Use It

This rule helps catch bugs, but you might disable it if:
  1. Your codebase uses loops for single-iteration patterns intentionally
  2. You’re using a framework that expects loop syntax for single operations
In most cases, refactoring to if statements will make code clearer.

Intentional Single Iteration

If you really need a loop that runs once:
/* eslint-disable no-unreachable-loop */
for (const key in obj) {
    firstKey = key;
    break; // Intentional: just get first key
}
/* eslint-enable no-unreachable-loop */

// But this is clearer:
const firstKey = Object.keys(obj)[0];