break and continue in JavaScript
break and continue in JavaScript are small keywords with a large effect on how a loop reads. They do not make a loop smarter. They change where execution jumps next. break leaves the loop. continue skips the rest of the current pass and moves to the next one. If you treat them as vague "skip stuff" commands, your loop will eventually skip the wrong thing.
The useful mental model is boring and exact: where does the program go after this line?
Contents
- The short rule
- break exits the loop
- continue skips one loop pass
- Order matters
- break and continue in while loops
- They only affect the nearest loop
- forEach, map, and callback confusion
- break in switch is related but different
- When to avoid them
- Key Takeaways
The short rule
Use break when the loop has finished its job early.
const tasks = ["write notes", "fix login", "fix checkout"];
let firstFixTask = "";
for (const task of tasks) {
if (task.includes("fix")) {
firstFixTask = task;
break;
}
}
console.log(firstFixTask); // "fix login"
Use continue when one item should be ignored, but the rest of the loop should keep running.
const scores = [82, -1, 94, 120, 75];
const validScores = [];
for (const score of scores) {
if (score < 0 || score > 100) {
continue;
}
validScores.push(score);
}
console.log(validScores); // [82, 94, 75]
You can practice those two shapes with the Break exercise and the Continue exercise.
For the full counter-loop shape around these keywords, see JavaScript for Loops. This article focuses on the control-flow jumps themselves.
break exits the loop
break stops the nearest loop immediately. JavaScript jumps to the first statement after that loop.
for (let number = 1; number <= 5; number += 1) {
if (number === 3) {
break;
}
console.log(number);
}
console.log("done");
The output is:
1
2
done
The loop does not print 3, 4, or 5. Once break runs, that loop is over.
That makes break a good fit for searches:
const users = [
{ id: 1, name: "Ada" },
{ id: 2, name: "Grace" },
{ id: 3, name: "Linus" },
];
let selectedUser = null;
for (const user of users) {
if (user.id === 2) {
selectedUser = user;
break;
}
}
console.log(selectedUser.name); // "Grace"
There is no reason to keep scanning after the matching user has been found. Continuing would be wasted work, and in some loops it can overwrite the first answer with a later one.
continue skips one loop pass
continue stops the current loop body, then moves to the next iteration.
for (let number = 1; number <= 5; number += 1) {
if (number === 3) {
continue;
}
console.log(number);
}
The output is:
1
2
4
5
Only the current pass is skipped. The loop is still alive.
That makes continue useful for guard clauses inside loops:
const orders = [
{ id: "a1", paid: true, shipped: false },
{ id: "b2", paid: false, shipped: false },
{ id: "c3", paid: true, shipped: true },
];
const readyToShip = [];
for (const order of orders) {
if (!order.paid) continue;
if (order.shipped) continue;
readyToShip.push(order.id);
}
console.log(readyToShip); // ["a1"]
The loop body says, in order: skip unpaid orders, skip already shipped orders, then process the only orders left.
Order matters
When a loop uses both break and continue, the order of the checks is part of the behavior.
for (let number = 1; number <= 10; number += 1) {
if (number % 2 === 0) {
continue;
}
if (number === 7) {
break;
}
console.log(number);
}
This logs:
1
3
5
Odd numbers pass the continue check. When number reaches 7, break stops the loop before 7 is logged.
Now change the stop value to an even number:
for (let number = 1; number <= 10; number += 1) {
if (number % 2 === 0) {
continue;
}
if (number === 8) {
break;
}
console.log(number);
}
This never stops at 8. The continue runs first, so JavaScript skips the break check for every even number. If the stopping condition must always be noticed, check it before any continue.
for (let number = 1; number <= 10; number += 1) {
if (number === 8) {
break;
}
if (number % 2 === 0) {
continue;
}
console.log(number);
}
Tiny change. Very different loop. This is why "just throw in a continue" is not much of a strategy.
break and continue in while loops
break and continue also work in while and do...while loops. The danger is that a continue can skip the line that updates the loop state.
This loop gets stuck:
let index = 0;
const names = ["Ada", "", "Grace"];
while (index < names.length) {
if (names[index] === "") {
continue;
}
console.log(names[index]);
index += 1;
}
When index is 1, the name is an empty string. continue jumps back to the condition before index += 1 runs. The next pass sees the same empty string again. Congratulations, the loop is now deeply committed to doing nothing forever.
Move the update before the possible continue, or use a for loop when the index update is mechanical.
let index = 0;
const names = ["Ada", "", "Grace"];
while (index < names.length) {
const name = names[index];
index += 1;
if (name === "") {
continue;
}
console.log(name);
}
For array and string values, a for...of loop often avoids this problem because there is no manual index update to forget.
They only affect the nearest loop
In a nested loop, break and continue affect the innermost loop they are inside.
for (let row = 1; row <= 3; row += 1) {
for (let col = 1; col <= 3; col += 1) {
if (col === 2) {
break;
}
console.log(`R${row}C${col}`);
}
}
The output is:
R1C1
R2C1
R3C1
The break stops the column loop. It does not stop the row loop. The next row still starts.
continue behaves the same way:
for (let row = 1; row <= 2; row += 1) {
for (let col = 1; col <= 3; col += 1) {
if (col === 2) {
continue;
}
console.log(`R${row}C${col}`);
}
}
The output is:
R1C1
R1C3
R2C1
R2C3
JavaScript has labeled statements that can target an outer loop, but they are uncommon and easy to overuse. If a nested loop needs labeled control flow, first ask whether the loop should become a function that can use return instead.
function hasBlockedCell(grid) {
for (const row of grid) {
for (const cell of row) {
if (cell === "blocked") {
return true;
}
}
}
return false;
}
The function return is often clearer than a label because it names the question the loop is answering.
forEach, map, and callback confusion
You cannot use break or continue inside an array callback unless that callback is itself inside a loop. This is a syntax error:
const numbers = [1, 2, 3];
numbers.forEach((number) => {
if (number === 2) {
break;
}
console.log(number);
});
forEach is a method call. The callback is a function. break and continue belong to loop and switch statements, not arbitrary functions.
Returning from the callback is not the same thing:
[1, 2, 3].forEach((number) => {
if (number === 2) {
return;
}
console.log(number);
});
This logs:
1
3
The return skips only that callback call. It does not stop forEach from calling the callback again for 3.
Use a real loop when loop control is the point:
for (const number of [1, 2, 3]) {
if (number === 2) {
break;
}
console.log(number);
}
For a broader comparison, see forEach vs map in JavaScript.
break in switch is related but different
break also exits a switch statement.
const role = "editor";
switch (role) {
case "admin":
console.log("can manage users");
break;
case "editor":
console.log("can edit content");
break;
default:
console.log("can read content");
}
In a switch, break prevents fall-through into the next case. In a loop, break exits the loop. Same keyword, different control structure.
If a switch sits inside a loop, break exits the switch, not the loop:
for (const role of ["viewer", "editor", "admin"]) {
switch (role) {
case "editor":
console.log("found editor");
break;
}
console.log("loop still running");
}
The switch guide has more on why break matters in switch statements.
When to avoid them
Avoid break and continue when they make the main path hard to see.
This is doing a little too much in one loop:
for (const user of users) {
if (!user.active) continue;
if (user.deleted) continue;
if (user.role === "admin") break;
if (!user.email) continue;
sendEmail(user.email);
}
The code may be correct, but the reader has to simulate several exits before understanding what counts as a sendable user.
Sometimes a named predicate is cleaner:
function canReceiveEmail(user) {
return user.active && !user.deleted && user.email;
}
for (const user of users) {
if (user.role === "admin") break;
if (!canReceiveEmail(user)) continue;
sendEmail(user.email);
}
The goal is not to ban control flow. The goal is to make each jump explainable.
Key Takeaways
breakexits the nearest loop immediately.continueskips the rest of the current loop pass and moves to the next one.- Check stopping conditions before
continuewhen the stop must always be noticed. - In
whileloops, make surecontinuedoes not skip the state update. - Use real loops, not
forEachormap, whenbreakandcontinueare part of the logic.