break, continue and Labels

Mental model: break leaves the loop, continue skips to the next pass, and a label says which loop you meant.

Level: beginner · about 9 minutes

for (const n of [1, 2, 3, 4, 5]) {
  if (n === 2) continue;   // skip the rest of THIS pass
  if (n === 4) break;      // leave the loop entirely
  console.log(n);          // → 1, then 3
}
console.log('done');

Two keywords, two very different jobs.

  • continue jumps to the update step and starts the next pass. Nothing after it in the body runs.
  • break abandons the loop and continues with the statement after it.
  • Both apply to the innermost enclosing loop, which is exactly the problem labels solve.
const rows = [
  { id: 1, price: 10, inStock: true },
  { id: 2, price: 99, inStock: false },
  { id: 3, price: 5, inStock: true },
];

let total = 0;
for (const row of rows) {
  if (!row.inStock) continue;   // one reason to skip, on its own line
  total += row.price;
}
console.log(total); // → 15

A guard clause inside a loop reads like a guard clause inside a function.

Labels: naming a loop so you can leave it

A plain break inside a nested loop only escapes the inner one, so the outer loop carries on searching after you already found what you wanted. A label is an identifier followed by a colon in front of a statement, and break label leaves that statement.

const grid = [[1, 2], [3, 4], [5, 6]];

search: for (const row of grid) {
  for (const cell of row) {
    if (cell === 4) {
      console.log('found 4');
      break search;      // leaves BOTH loops
    }
    console.log('checked', cell);
  }
}
console.log('after the loops');

Without a label: a flag

let found = false;
for (const row of grid) {
  for (const cell of row) {
    if (cell === 4) { found = true; break; }
  }
  if (found) break;
}

With a label: no flag

search: for (const row of grid) {
  for (const cell of row) {
    if (cell === 4) break search;
  }
}

continue label also exists and means "start the next pass of the labelled loop". Labels are rare enough that a reader will slow down when they see one, so keep the name descriptive: search, outer, rows.

function findCell(grid, wanted) {
  for (const [r, row] of grid.entries()) {
    for (const [c, cell] of row.entries()) {
      if (cell === wanted) return [r, c];   // no label needed
    }
  }
  return null;                              // say "not found" out loud
}

console.log(findCell([[1, 2], [3, 4]], 4)); // → [1, 1]
console.log(findCell([[1]], 9));            // → null

some and every are early exits with names

const nums = [1, 3, 6, 7, 9];

const checked = [];
const hasEven = nums.some((n) => {
  checked.push(n);
  return n % 2 === 0;
});

console.log(hasEven);   // → true
console.log(checked);   // → [1, 3, 6]  it stopped as soon as it knew
QuestionMethodStops as soon as
Is at least one true?somethe callback returns truthy
Are all of them true?everythe callback returns falsy
Which element matches?findthe callback returns truthy
Where is it?findIndexthe callback returns truthy
Is this exact value present?includesa strict match is found
let output = '';
for (let i = 0; i < 5; i++) {
  if (i % 2 === 0) continue;
  if (i > 3) break;
  output += i;
}
console.log(output);

Even values are skipped by continue, so only 1 and 3 reach the bottom of the body. When i is 5 the loop condition has already failed, so the break at i > 3 never fires. The result is the string '13', built by += on a string.

Try it yourself

Escape the nested loop three ways

const grid = [
  ['a', 'b', 'c'],
  ['d', 'e', 'f'],
  ['g', 'h', 'i'],
];

let visited = 0;
outer: for (const row of grid) {
  for (const cell of row) {
    visited += 1;
    if (cell === 'e') break outer;
  }
}
console.log('labelled break visited', visited, 'cells');

visited = 0;
let found = false;
for (const row of grid) {
  for (const cell of row) {
    visited += 1;
    if (cell === 'e') { found = true; break; }
  }
  if (found) break;
}
console.log('flag version visited', visited, 'cells');

Comment out the labelled version and make the flag version behave identically. Then rewrite it as a function with a return and compare the line counts.

Exercises

Sum the even numbers with continue

Write sumEven(numbers) which adds up only the even values. Use a continue guard for the odd ones rather than wrapping the addition in an if, so the loop body stays flat.

Find the first pair with a labelled break

Write findFirstPair(left, right, target). Walk left in the outer loop and right in the inner loop, and return [x, y] for the first pair whose sum equals target. Return null when nothing matches. Use a labelled break so the outer loop stops as soon as you have an answer.

Check yourself

What does this log?
[1] — 1 is pushed. 2 is skipped by continue. 3 ends the loop before the push, so 4 is never reached. Only [1] survives.
A plain break inside two nested loops leaves…
the innermost loop only — break always applies to the nearest enclosing loop or switch. To leave an outer loop you either label it and use break label, keep a flag, or extract the loops into a function and return.
Why does some often replace a for loop with a break?
It short circuits on the first truthy result, so it does the same work and reads as a question — some stops calling the callback as soon as one returns truthy, exactly like a break. The gain is intent: the code now says "is there at least one" instead of describing the mechanics.

Common mistakes

  • Using continue in a while loop above the increment, which makes the loop run forever.
  • Expecting a plain break in a nested loop to leave both loops.
  • Reaching for forEach and then discovering there is no way to stop it.

Takeaways

  • continue skips the rest of the current pass; break leaves the loop.
  • Both target the innermost loop unless you label the one you mean.
  • Extracting the loops into a function and returning is usually cleaner than either a label or a flag.
  • some, every, find and findIndex are early exits with intent baked into the name.