goto (and why to avoid it)
goto is an unconditional jump. Instead of the normal top-to-bottom, function-call, loop-and-branch flow that every other PHP construct gives you, goto label; tells the engine "stop executing here, and resume execution at this other line instead." PHP has supported it since version 5.3, largely because a handful of code-generation and low-level use cases genuinely need it. In almost every other situation it is the wrong tool: it lets control flow jump anywhere in a function with no structure attached to the jump, which is exactly the property that makes code hard to read, hard to test, and hard to debug. This page explains the syntax so you can recognize it when you see it, shows the rare cases where it earns its place, and then makes the case — with examples — for reaching for break, continue, early return, or a restructured condition instead.
Syntax and label rules
A label is just an identifier followed by a colon, placed on its own line inside the same function, file scope, or included script as the goto that targets it. When the engine executes goto label;, it immediately transfers control to the line right after that label, skipping everything in between (or jumping backward, if the label sits earlier in the script).
A basic goto jump
<?php echo "Step 1\n"; goto skip; echo "Step 2 (never runs)\n"; skip: echo "Step 3\n";
Step 1 Step 3
Notice that "Step 2" never prints — the jump landed past it entirely. goto can also jump backward to re-run earlier code, effectively building a loop by hand.
A hand-rolled loop using goto (don't do this — shown for illustration)
<?php
$i = 0;
start:
if ($i < 3) {
echo "i = {$i}\n";
$i++;
goto start;
}
echo "done\n";i = 0 i = 1 i = 2 done
PHP does place real restrictions on where a jump can land, precisely because unrestricted jumps would make loops and switch statements impossible to reason about. You cannot jump into a loop, a switch block, or a foreach from outside it — the engine raises a fatal error at compile time. You can jump out of a loop or switch to a label further down the function, which is the one pattern PHP's manual actually documents as intentional.
Illegal: jumping into a loop body from outside
<?php
goto inside;
for ($i = 0; $i < 5; $i++) {
inside:
echo $i;
}
// Fatal error: 'goto' into loop or switch statement is disallowedThe one legitimate use case: escaping deeply nested loops
The scenario PHP's own documentation uses to justify goto is breaking out of several levels of nested loops at once, from deep inside the innermost one, in a single statement. Compare the same search written three ways.
Version 1: goto to escape three nested loops
<?php
$target = 42;
$found = false;
for ($i = 0; $i < 10; $i++) {
for ($j = 0; $j < 10; $j++) {
for ($k = 0; $k < 10; $k++) {
if ($i * $j * $k === $target) {
$found = true;
goto done;
}
}
}
}
done:
echo $found ? "Found a match\n" : "No match\n";Version 2: break with a level argument (usually preferable)
<?php
$target = 42;
$found = false;
for ($i = 0; $i < 10; $i++) {
for ($j = 0; $j < 10; $j++) {
for ($k = 0; $k < 10; $k++) {
if ($i * $j * $k === $target) {
$found = true;
break 3; // breaks out of all three loops at once
}
}
}
}
echo $found ? "Found a match\n" : "No match\n";Version 3: extract into a function and return (usually best)
<?php
function hasMatch(int $target): bool
{
for ($i = 0; $i < 10; $i++) {
for ($j = 0; $j < 10; $j++) {
for ($k = 0; $k < 10; $k++) {
if ($i * $j * $k === $target) {
return true; // return exits immediately, no flags needed
}
}
}
}
return false;
}
echo hasMatch(42) ? "Found a match\n" : "No match\n";All three versions produce identical output, but they are not equally readable. Version 1 needs a $found flag, a label placed after the loops, and a mental jump to see where control actually goes. Version 2's break 3 states its intent in three characters and needs no flag. Version 3 goes further: wrapping the search in a function means return can exit from any depth of nesting with no flag and no label at all, and the function boundary documents exactly what the block of code is for. In practice, break N or a function-and-return refactor covers essentially every scenario people reach for goto to solve — the honest reason to know goto exists is to recognize it in code you are reviewing, not to reach for it yourself.
Where goto actually shows up
Outside of textbook examples, the realistic place you will encounter goto in PHP is generated or templated code — compilers, transpilers, template engines, or code emitted by a parser generator — where a machine is producing the control flow rather than a human typing it out. A generator can mechanically emit a goto for every possible control-flow edge without needing to "understand" nesting the way a human author would when reaching for break or restructuring a function. If you are writing PHP by hand for a web application, this case essentially never applies to you.
Alternatives to reach for instead
break N;— exit N levels of nested loops or aswitchin one statement, no label required.continue N;— skip to the next iteration N levels up, when you need to move on rather than exit entirely.Early
returnfrom a function — the cleanest way to bail out of deep nesting; extracting a chunk of logic into its own function is often the real fix.Restructuring the condition — inverting an
ifto return/continue early, or combining conditions with&&/||, frequently removes the nesting that made a jump feel necessary in the first place.