DEV Community

JKC
JKC

Posted on

Stop Setting Flags to Escape Nested Loops. C# 15 Has Labeled break

TL;DR: In C# 15, break and continue can name an outer loop: break search;, continue orders;. It ships with .NET 11, which is at Release Candidate 1 right now: go-live licensed, but not final yet.

The flag nobody wants to own

You need to find one value in a grid and stop. Simple, right?

int[][] grid = [[1, 2, 3], [4, 42, 6], [7, 8, 9]];
(int Row, int Col)? hit = null;

bool found = false;
for (int r = 0; r < grid.Length; r++)
{
    for (int c = 0; c < grid[r].Length; c++)
    {
        if (grid[r][c] == 42)
        {
            hit = (r, c);
            found = true;
            break;
        }
    }

    if (found)
        break;
}
Enter fullscreen mode Exit fullscreen mode

A plain break only leaves the innermost loop. So you invent a bool, set it, break, check it, break again. Add a third loop and you check it twice.

The other classic exit is goto. It works. It also makes your code reviewer reach for coffee.

Meet labeled break and continue

Put a label on the loop. Then name it when you jump. Same grid, no flag:

search: for (int r = 0; r < grid.Length; r++)
{
    for (int c = 0; c < grid[r].Length; c++)
    {
        if (grid[r][c] == 42)
        {
            hit = (r, c);
            break search;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

break search; leaves the loop labeled search, and execution carries on after it. continue search; would skip to the next iteration of that loop instead. Both versions above printed (1, 1) for the same grid when I ran them on the .NET 11 RC1 SDK.

If you have written Java, JavaScript, Go, Rust, Kotlin or Swift, this will feel familiar. C# just took a couple of decades to join the party.

Unlabeled break and continue work exactly as before.

A realistic one: importing orders

Here is an import loop with two rules:

  • A line with no SKU makes the whole order bad, so skip to the next order.
  • A negative quantity means the file itself is broken, so stop the import.

One label handles both:

var orders = new[]
{
    new Order(1001, [new("SKU-1", 2), new("SKU-2", 1)]),
    new Order(1002, [new("SKU-3", 1), new("", 4), new("SKU-4", 1)]),
    new Order(1003, [new("SKU-5", 0)]),
    new Order(1004, [new("SKU-6", -1), new("SKU-7", 3)]),
    new Order(1005, [new("SKU-8", 1)]),
};

var accepted = new List<int>();

orders: foreach (var order in orders)
{
    foreach (var line in order.Lines)
    {
        if (line.Sku == "")
        {
            Console.WriteLine($"Order {order.Id}: missing SKU, skipping order");
            continue orders;
        }

        if (line.Quantity < 0)
        {
            Console.WriteLine($"Order {order.Id}: negative quantity, stopping import");
            break orders;
        }
    }

    accepted.Add(order.Id);
}

Console.WriteLine($"Accepted: {string.Join(", ", accepted)}");

record Line(string Sku, int Quantity);
record Order(int Id, Line[] Lines);
Enter fullscreen mode Exit fullscreen mode

Output from the run:

Order 1002: missing SKU, skipping order
Order 1004: negative quantity, stopping import
Accepted: 1001, 1003
Enter fullscreen mode Exit fullscreen mode

Order 1002 never reaches accepted.Add, because continue orders jumps straight to the next order. Order 1005 is never even looked at, because break orders ended the whole thing. With goto you would need two labels in two different places for this.

Bonus: leaving a loop from inside a switch

This one has bitten everybody once. Inside a switch, break ends the switch, not the loop around it. Now you can say which one you mean:

string[] commands = ["add", "add", "quit", "add"];
int added = 0;

loop: foreach (var cmd in commands)
{
    switch (cmd)
    {
        case "add": added++; break;
        case "quit": break loop;
    }
}

Console.WriteLine($"Added: {added}"); // Added: 2
Enter fullscreen mode Exit fullscreen mode

Can I use it?

  • Labeled break and continue are part of C# 15. On the .NET 11 RC1 SDK, #error version reports Language version: 15.0 with no LangVersion set, so there is nothing to switch on.
  • .NET 11 RC1 shipped on September 8, 2026 with a go-live support license, so it is allowed in production. It is still a release candidate, though, not the final release.
  • On the .NET 10 SDK (C# 14), the same code fails with CS8652: The feature 'labeled break and continue' is currently in Preview and *unsupported*.

I ran every snippet here as a file-based app with dotnet run file.cs on SDK 11.0.100-rc.1.

Gotchas

1. The label must sit directly on a loop or a switch. Labeling a block and breaking out of it does not work:

block:
{
    for (int i = 0; i < 3; i++) { break block; }
}
// error CS9393: No enclosing loop or switch statement with the label 'block' out of which to break
Enter fullscreen mode Exit fullscreen mode

2. continue only targets loops. You can break a labeled switch, but continue sw; on one gives CS9394: No enclosing loop with the label 'sw' out of which to continue.

3. Stacked labels: only the closest one counts. In a: b: while (true) { break a; }, the loop is labeled b, not a. The compiler answers with CS9393 again. One loop, one name.

4. It cannot escape a lambda. A break outer; inside an Action that sits in the loop gives CS1632: Control cannot leave the body of an anonymous method or lambda expression. A lambda is its own function, so the outer loop is not "enclosing" anymore.

5. finally still runs. A labeled jump out of a try block runs the finally on the way out, just like a normal break. In my test, break outer from inside a try printed finally ran for i=0 and then left both loops.

6. Your IDE will start nagging. The new style rule IDE0410, "Use labeled jump statement", flags the bool flag and goto patterns and offers the labeled version. Turn it off with csharp_style_prefer_labeled_jump_statements = false if you are not ready for that conversation.

Wrap-up

Sometimes the right fix is still pulling the loops into a method and using return. But when the nested loop really belongs where it is, a label says exactly what you mean. Go find that bool found = false; in your codebase and give it a well-earned retirement.

Sources

This article was written with the help of AI and checked against the sources above.

Top comments (0)