Labeled break and continue Statements

Summary

Allow break and continue statements to optionally specify a label that identifies which loop or switch statement to target, enabling cleaner control flow in nested constructs without requiring goto statements, or other contortions like nested functions, tuple returns, etc.

Motivation

When working with nested loops or loops containing switch statements, developers often need to break out of or continue an outer loop from within an inner context. Currently, there are two primary approaches to achieve this, both with significant drawbacks:

Using goto statements

string foundValue = null;
for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        foundValue = GetValue(x, y);
        if (foundValue == target)
            goto FOUND;
    }
}
FOUND:
ProcessValue(foundValue);

While goto works, it requires placing labels after the loop construct and doesn't clearly communicate the intent to break from a specific loop. For continuing an outer loop, the approach becomes even more awkward:

for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            goto CONTINUE_OUTER;
    }
    CONTINUE_OUTER: ;
}

This pattern is confusing because the label must be placed at the end of the loop body, just before the closing brace, so that the incrementor and condition checking happen. When both break and continue are needed for the same outer loop, two separate labels are required:

for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            goto CONTINUE_OUTER;
        
        if (ShouldExitAll(x, y))
            goto BREAK_OUTER;
    }
    CONTINUE_OUTER: ;
}
BREAK_OUTER:
// Subsequent statements

Using flag variables

string foundValue = null;
bool shouldBreak = false;
for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        foundValue = GetValue(x, y);
        if (foundValue == target)
        {
            shouldBreak = true;
            break;
        }
    }
    if (shouldBreak)
        break;
}
ProcessValue(foundValue);

This approach requires additional state management, increases code verbosity, and obscures the control flow intent.

Proposed solution

With labeled break and continue, the code becomes clearer and more maintainable:

string foundValue = null;
outer: for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        foundValue = GetValue(x, y);
        if (foundValue == target)
            break outer;
    }
}
ProcessValue(foundValue);

The label is placed directly on the loop it identifies, and the break/continue statement explicitly names its target. For continuing:

outer: for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            continue outer;
    }
}

This naturally expresses "continue the outer loop," without the confusion of label placement associated with goto. A single label can be used for both operations:

outer: for (int x = 0; x < xMax; x++)
{
    for (int y = 0; y < yMax; y++)
    {
        if (ShouldSkipRest(x, y))
            continue outer;
        
        if (ShouldExitAll(x, y))
            break outer;
    }
}

This feature has been requested extensively in the C# community, with discussions dating back decades and the topic being reintroduced and rerequested continuously. Similar features exist in several other modern languages:

In all these cases, the languages operate in the same way as in this specification. Namely, some constructs can have a label, and it is possible to reference that label from their respective continue or break statements.

Detailed design

The following updates are presented as a diff against the corresponding sections of the C# 7 standard (statements.md). Throughout this section, strikethrough indicates text being removed from the existing specification, and bold indicates text being added. Unchanged prose is quoted verbatim for context.

§13.5 Labeled statements

Insert the following paragraph immediately after the existing paragraph "A label can be referenced from goto statements (§13.10.4) within the scope of the label.":

If the statement immediately nested within a labeled_statement is a switch_statement (§13.8.3) or an iteration_statement (§13.9), the nested statement is said to be labeled with the identifier of the labeled_statement. A break_statement (§13.10.2) or continue_statement (§13.10.3) can specify such an identifier to reference the containing labeled statement.

Note: Only the statement that is immediately nested within a labeled_statement is labeled with that identifier. For example, given a: b: while (…) …, only b labels the iteration_statement; a labels the inner labeled_statement b: while (…) …, which is not itself a switch_statement or iteration_statement. Consequently, break a; or continue a; appearing within the loop body does not target the while statement. end note

§13.10.2 The break statement

break_statement
    : 'break' identifier? ';'
    ;

The break statement exits the nearest enclosing switch, while, do, for, or foreach statement.

The break statement exits the nearest enclosing switch_statement (§13.8.3) or iteration_statement (§13.9), or, if an identifier is specified, the nearest enclosing switch_statement or iteration_statement labeled with that identifier (see §13.5).

The target of a break statement is the end point of the nearest enclosing switch, while, do, for, or foreach statement statement determined as above. If a break statement is not enclosed by a switch, while, do, for, or foreach statement, a compile-time error occurs. If no such enclosing statement exists, a compile-time error occurs.

When multiple switch, while, do, for, or foreach statements are nested within each other, a break statement applies only to the innermost statement. To transfer control across multiple nesting levels, a goto statement (§13.10.4) shall be used.

A break statement cannot exit a finally block (§13.11). When a break statement occurs within a finally block, the target of the break statement shall be within the same finally block; otherwise a compile-time error occurs.

A break statement is executed as follows:

  • If the break statement exits one or more try blocks with associated finally blocks, control is initially transferred to the finally block of the innermost try statement. When and if control reaches the end point of a finally block, control is transferred to the finally block of the next enclosing try statement. This process is repeated until the finally blocks of all intervening try statements have been executed.
  • Control is transferred to the target of the break statement.

Because a break statement unconditionally transfers control elsewhere, the end point of a break statement is never reachable.

Example: A labeled break resolves to the nearest enclosing switch_statement or iteration_statement with the matching label:

outer: for (int i = 0; i < 10; i++)
{
    for (int j = 0; j < 10; j++)
    {
        if (i * j > 20)
            break outer; // exits the outer for-loop
    }
}

end example

§13.10.3 The continue statement

continue_statement
    : 'continue' identifier? ';'
    ;

The continue statement starts a new iteration of the nearest enclosing while, do, for, or foreach statement.

The continue statement starts a new iteration of the nearest enclosing iteration_statement (§13.9), or, if an identifier is specified, the nearest enclosing iteration_statement labeled with that identifier (see §13.5).

The target of a continue statement is the end point of the embedded statement of the nearest enclosing while, do, for, or foreach statement iteration_statement determined as above. If a continue statement is not enclosed by a while, do, for, or foreach statement, a compile-time error occurs. If no such enclosing statement exists, a compile-time error occurs.

When multiple while, do, for, or foreach statements are nested within each other, a continue statement applies only to the innermost statement. To transfer control across multiple nesting levels, a goto statement (§13.10.4) shall be used.

A continue statement cannot exit a finally block (§13.11). When a continue statement occurs within a finally block, the target of the continue statement shall be within the same finally block; otherwise a compile-time error occurs.

A continue statement is executed as follows:

  • If the continue statement exits one or more try blocks with associated finally blocks, control is initially transferred to the finally block of the innermost try statement. When and if control reaches the end point of a finally block, control is transferred to the finally block of the next enclosing try statement. This process is repeated until the finally blocks of all intervening try statements have been executed.
  • Control is transferred to the target of the continue statement.

Because a continue statement unconditionally transfers control elsewhere, the end point of a continue statement is never reachable.

Example: A labeled continue resolves to the nearest enclosing iteration_statement with the matching label:

outer: for (int i = 0; i < 10; i++)
{
    for (int j = 0; j < 10; j++)
    {
        if (ShouldSkip(i, j))
            continue outer; // continues the outer for-loop
    }
}

end example

Drawbacks/Alternatives

Keep using goto statements

C# already supports goto, which can accomplish the same control flow. However, goto has several disadvantages compared to labeled break/continue:

  • Requires separate labels for break vs. continue scenarios (break labels go after the loop, continue labels go before the closing brace)
  • Label placement is less intuitive and differs based on whether you're breaking or continuing
  • Less explicit about intent (jumping to a location vs. breaking/continuing a specific loop)
  • Brittle and error-prone: developers must ensure no statements are accidentally placed between labels and their target constructs. For example, with goto END_LOOP; followed by END_LOOP:, it's easy to inadvertently insert a statement between them during maintenance, breaking the intended control flow. Labeled loops prevent this issue by binding the label directly to the construct.
  • Carries historical stigma that labeled break/continue avoids

Use flag variables

As shown in the motivation section, flag variables work but add significant boilerplate and obscure the control flow logic.

Use break N or continue N with numeric levels

  • Fragile during refactoring (adding/removing a loop level requires updating all numeric references)
  • Harder to read (must count levels to understand the target)
  • Less explicit than named labels
  • Lack of clarity (1-based? 0-based?)

Refactor into separate methods

While this is often good practice, it's not always feasible or appropriate, and sometimes introduces unnecessary complexity for what should be simple control flow.

This proposal consolidates and addresses the following community discussions:

Open questions

label semantics

The current specification defines the semantics for break identifier/continue identifier as finding the innermost applicable loop/switch construct labeled with that identifier, and then dispatching to it with standard break/continue semantics. An alternative formalization is to instead say that break identifier/continue identifier identifies a label using the same rules as 'goto'. And if the label directly contains a loop/switch construct that encloses the break/continue, then that is the loop/switch that the break/continue applies to.

Both formalization are effectively identical, allowing and disallowing the same set of programs. The approach chosen in this speclet was done for both conceptual and literal simplicity. It does not have to cover scoping of labels, or the binding of identifiers as goto does it, or having to definie outward-in binding logic for resolving the loop/switch and continue/break statement. Instead, it simply expands the simple spec language that finds the appropriate enclosing loop/switch given the break/continue, allowing it to extend past the innermost, to something above that.

If LDM feels tying this tighter to goto+label semantics, it would not be difficult to adjust the spec to that. Keeping this question open if the group feels the latter form is more natural than the form taken here.

void M()
{
  label:
  Console.WriteLine();
  
  foreach (var x in ...)
  {
    break label;
    // should this scenario fail because:
    // 1. the identifier lookup fails, or
    // 2. the label is rejected (not a valid label for a `break` since not attached to a loop construct) after being found?
  }
}

Spec on how labels are declared:

Each block or switch_block creates a separate declaration space for labels. Names are introduced into this declaration space through labeled_statements, and the names are referenced through goto_statements. The label declaration space of a block includes any nested blocks. Thus, within a nested block it is not possible to declare a label with the same name as a label in an enclosing block.

Spec on labeled statements:

The scope of a label is the whole block in which the label is declared, including any nested blocks. It is a compile-time error for two labels with the same name to have overlapping scopes.
A label can be referenced from goto statements (§13.10.4) within the scope of the label.

nested labels

Should a: b: while (true) continue a; be supported?

Recommendation: no. No users have asked for this. No compelling use cases have been presented for it. Most mainstream languages do not allow it, with no complaints from their communities. The lang (and impl) are simpler and clearer bring strict that only the direct containing labeled statement labels the loop/switch.

Design meetings

TBD