Nested loops are easy to write.
Getting out of them cleanly isn't always as easy.
Consider a simple grid search:
for (int row = 0; row < grid.Height; row++)
{
for (int column = 0; column < grid.Width; column++)
{
if (grid[row, column].IsGoal)
{
// How do we exit both loops?
}
}
}
A normal break isn't enough:
break;
It only exits the innermost loop.
Until now, C# developers typically solved this with a Boolean flag, an early return, or occasionally goto.
C# 15 adds another option:
labeled break and continue.
The problem with a normal break
Suppose we're looking for a value inside a matrix:
bool found = false;
for (int row = 0; row < matrix.Length; row++)
{
for (int col = 0; col < matrix[row].Length; col++)
{
if (matrix[row][col] == target)
{
found = true;
break;
}
}
if (found)
break;
}
It works.
But notice what found is doing.
It isn't really application state.
It exists only to propagate a break from one loop to another.
With more deeply nested loops, this pattern becomes increasingly awkward.
C# 15: name the loop
C# 15 lets us place a label directly on the loop:
outer: for (int row = 0; row < matrix.Length; row++)
{
for (int col = 0; col < matrix[row].Length; col++)
{
if (matrix[row][col] == target)
{
break outer;
}
}
}
That's the important new syntax:
outer: for (...)
and:
break outer;
Instead of breaking only the inner loop, C# knows that we want to exit the loop labeled outer.
Execution continues immediately after that loop.
The Boolean flag disappears completely.
It also works with continue
The same idea applies to continue.
Imagine processing a collection of orders.
If any item in an order is invalid, we want to abandon that order and immediately move to the next one.
Without labeled continue, we might need another flag:
foreach (var order in orders)
{
bool invalid = false;
foreach (var item in order.Items)
{
if (!item.IsValid)
{
invalid = true;
break;
}
Process(item);
}
if (invalid)
continue;
Complete(order);
}
In C# 15:
ordersLoop: foreach (var order in orders)
{
foreach (var item in order.Items)
{
if (!item.IsValid)
{
continue ordersLoop;
}
Process(item);
}
Complete(order);
}
When this executes:
continue ordersLoop;
C# immediately starts the next iteration of the labeled outer loop.
There is no temporary flag and no additional condition after the inner loop.
Microsoft's documentation specifically describes labeled jumps as a replacement for Boolean flags and some goto patterns used to escape nested control flow.
What about goto?
C# could already escape nested loops using goto:
for (...)
{
for (...)
{
if (found)
goto Finished;
}
}
Finished:
Console.WriteLine("Done");
That works, and goto isn't inherently evil.
But labeled break communicates the intent more precisely:
search: for (...)
{
for (...)
{
if (found)
break search;
}
}
We're not saying:
Jump to this arbitrary position.
We're saying:
Stop this specific loop.
That's a useful distinction.
C# 15 even introduces analyzer support through IDE0410, which can identify some Boolean-flag and goto patterns that can be replaced with labeled jumps.
break and continue aren't identical
There is one small distinction worth remembering.
A labeled break can target an enclosing:
for
foreach
while
do
switch
A labeled continue can only target an enclosing loop, because continuing a switch doesn't make sense.
And existing code doesn't change.
This:
break;
still exits the nearest applicable construct.
This:
continue;
still continues the nearest loop.
You only get the new behavior when you explicitly provide a label.
Should you start labeling every loop?
Probably not.
Most loops don't need it.
And if you have five levels of nested control flow, extracting some logic into another method may still produce cleaner code.
But there are legitimate cases where nested loops are the simplest representation of the problem:
- scanning matrices
- traversing multidimensional data
- processing batches and their items
- parsing nested structures
- searching combinations
In those situations, labeled jumps can remove bookkeeping variables without forcing you to restructure otherwise straightforward code.
Trying it today
C# 15 is currently available through the .NET 11 preview SDK and Visual Studio 2026 Insiders.
The feature itself is surprisingly small.
Before:
bool stop = false;
// loops...
if (stop)
break;
Now:
outer: for (...)
{
for (...)
{
break outer;
}
}
Sometimes language improvements aren't about enabling something completely new.
They're about letting the code say more clearly what you already wanted it to do.
And labeled break and continue are a nice example of exactly that.
Top comments (0)