PHPbreak & continue

break & continue

Loops normally run from start to finish, checking their condition once per pass. break and continue are the two keywords that let you step outside that default rhythm from inside the loop body. break stops the loop immediately and jumps to the first line after it; continue skips the rest of the current pass and jumps straight to the next iteration's condition check. Both work inside a for loop, a foreach loop, and a while loop, and both accept an optional numeric argument that lets them reach through nested loops — a feature that is easy to overuse.

break: stopping a loop early

The most common use of break is a search: you loop over a collection looking for something, and once you find it there is no reason to keep checking the rest. Without break you would either waste cycles scanning items you no longer care about, or you would need an extra flag variable to remember "found it, stop doing work."

Search until found

PHP
<?php
$usernames = ["amir", "beth", "carlos", "dina", "erin"];
$target = "carlos";
$foundAt = -1;

foreach ($usernames as $index => $name) {
    if ($name === $target) {
        $foundAt = $index;
        break; // no point scanning "dina" or "erin"
    }
}

echo $foundAt === -1 ? "Not found" : "Found at index {$foundAt}";
Found at index 2
continue: skipping just this iteration

continue is for the opposite situation: you still want the loop to keep running, you just want to skip the work for the current item. This shows up constantly when filtering values while you loop, rather than building a separate filtered array first.

Skip values you do not want to process

PHP
<?php
$numbers = [4, 7, 10, 13, 18, 21, 24];

foreach ($numbers as $n) {
    if ($n % 2 !== 0) {
        continue; // odd numbers are skipped entirely
    }
    echo $n . " ";
}
4 10 18 24 

Notice that 21 and 7 never reach the echocontinue jumps back to the top of the loop the moment it runs, so any code written after it in that iteration is simply never executed.

The optional level argument

Both keywords accept an integer argument, written as break 2 or continue 2, which tells PHP how many enclosing loops to affect. break 1 and continue 1 (the default when you omit the number) only touch the innermost loop, exactly like the examples above. break 2 breaks out of the innermost loop and the one around it; continue 2 skips to the next iteration of the loop two levels up, not the one you are currently in.

Nested loops without a level argument

PHP
<?php
$grid = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
];

foreach ($grid as $row) {
    foreach ($row as $cell) {
        if ($cell === 5) {
            break; // only stops the INNER loop
        }
        echo $cell . " ";
    }
}
1 2 3 4 

The plain break above only escapes the inner foreach; the outer loop happily moves on to the row containing 7, 8, 9. If the goal was to stop scanning the whole grid the moment 5 is found, a single break is not enough — that is exactly what the level argument is for.

Nested loops with break 2

PHP
<?php
$grid = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
];

foreach ($grid as $row) {
    foreach ($row as $cell) {
        if ($cell === 5) {
            break 2; // stops BOTH loops
        }
        echo $cell . " ";
    }
}
1 2 3 4 

The printed output looks identical to the previous example, but the difference matters: after this loop finishes, execution has left the outer foreach entirely, so the row 7, 8, 9 is never visited at all. With plain break, the outer loop would still iterate over that third row (it just would not print anything extra in this particular example, since the inner loop resets and would print 7 8 9 too if the condition never matched again).

continue 2 skips an iteration of the outer loop

PHP
<?php
for ($row = 1; $row <= 3; $row++) {
    for ($col = 1; $col <= 3; $col++) {
        if ($col === 2 && $row === 2) {
            continue 2; // abandon the rest of row 2 entirely
        }
        echo "r{$row}c{$col} ";
    }
}
r1c1 r1c2 r1c3 r2c1 r3c1 r3c2 r3c3 

When $row is 2 and $col reaches 2, continue 2 does not just skip to $col = 3 in the same row — it abandons row 2 entirely and jumps straight to the outer loop's next increment, which is $row = 3. That is why r2c2 and r2c3 never appear in the output.

Counting levels is easy to get wrong
The number after `break`/`continue` counts loops, not lines of code, and it counts from the innermost loop outward starting at `1`. It is easy to add or remove a loop while refactoring and forget to update the number, which silently changes which loop is affected instead of raising an error. There is no compiler warning for "this level argument no longer points where you think it does" — PHP will just break or continue the wrong loop.
Readability: when multi-level control flow hurts

break 2 and continue 2 are genuinely useful, but they ask the reader to hold two loops' state in their head at once and mentally count braces to figure out which loop a bare number refers to. In a short, two-level nested loop like the grid example above, that is a reasonable ask. In a three- or four-level nested loop, or one where the loops are separated by many lines of other logic, a break 3 or continue 2 buried in the middle becomes a small puzzle every time someone re-reads the code.

A common alternative is to extract the nested loops into their own function and use an ordinary return instead of a multi-level break. A return only ever exits the function it is written in, so there is nothing to count — the reader immediately knows "this stops everything below."

Replacing break 2 with an early return

PHP
<?php
function findCell(array $grid, int $target): ?array
{
    foreach ($grid as $rowIndex => $row) {
        foreach ($row as $colIndex => $cell) {
            if ($cell === $target) {
                return [$rowIndex, $colIndex]; // exits the function outright
            }
        }
    }

    return null;
}

$grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
$position = findCell($grid, 5);

echo $position === null
    ? "Not found"
    : "Found at row {$position[0]}, column {$position[1]}";
Found at row 1, column 1

This does the same job as the break 2 version, but the control flow reads top to bottom without needing to count loop levels. This is not a rule that multi-level break/continue should never be used — for a quick script or a genuinely simple two-level loop, the level argument is perfectly readable. Reach for the function-and-return version once the nesting gets deep enough that counting levels stops being obvious at a glance.

  • break exits the current loop immediately; code after it in the loop never runs.

  • continue skips the rest of the current iteration and moves on to the next one.

  • break N / continue N count outward from the innermost loop, starting at 1.

  • These keywords work the same way across for, foreach, and while loops.

  • Deeply nested loops with level arguments are a good candidate for extracting into a function with an early return.

switch counts as a loop for break
A plain `break` inside a `switch` statement only exits the `switch`, not any loop that happens to contain it. If a `switch` sits inside a loop and you want to break the loop from within a `case`, you need `break 2` — the `switch` block itself counts as one level.
Tip
Keep multi-level `break`/`continue` to at most two levels deep. If you find yourself writing `break 3` or reaching for `continue` across three loops, it is usually a sign the logic wants to be its own named function — the function boundary documents intent far better than a number does.