C# 15 New Features: Examples and Compiler Errors (.NET 11)

Last updated: October 29, 2026 · Tested with the .NET 11 RC3 SDK (11.0.100-rc.3.26522.148), C# 15, .NET 10.0.12 and .NET Framework 4.8 for the compatibility checks, Windows 11

Short answer: C# 15 ships with .NET 11 and adds union types, closed class hierarchies, labeled break and continue, collection expression arguments (with(...)), extension indexers, and static interface members that work on older runtimes. It’s the default language version for net11.0 projects. I compiled and ran each feature on the RC3 SDK, then broke each one on purpose to get the real compiler errors. Union types have their own tested post; this one covers everything else.

Feature What it does Errors you’ll meet
Labeled break / continue Exit or continue an outer loop by name CS9393, CS9394, CS1632
Collection expression arguments [with(capacity: 100), .. items] passes constructor arguments CS9354, CS9355, CS9357
Extension indexers this[...] inside an extension block CS9303
Closed hierarchies closed class: exhaustive switch without a default arm CS8509, CS9381, CS0656 on .NET 10
Static interface members on older runtimes Non-virtual static helpers in interfaces on .NET Framework CS9327 on C# 14, CS8701 for default members
Union types union Pet(Cat, Dog) see the union types post

How I Tested It

One console app (net11.0) uses every feature and prints the results. A small script compiles each mistake as its own project and saves what the compiler says; the error lines below are copied from those builds. The compatibility checks build the same code for net10.0 and net48 with the .NET 11 SDK. The memory safety feature is a preview behind a feature flag, so I left it out.

1. Labeled break and continue

Breaking out of nested loops has been a C# question for as long as C# has existed; the classic Stack Overflow question about it has over 66,000 views. The answers were a bool flag, a goto, or moving the loops into a method so you can return. C# 15 lets you name the loop:

outer: for (int row = 0; row < grid.GetLength(0); row++)
{
    for (int column = 0; column < grid.GetLength(1); column++)
    {
        if (grid[row, column] == 0)
        {
            Console.WriteLine($"row {row}: blocked at column {column}, next row");
            continue outer;
        }
        if (grid[row, column] == 9)
        {
            Console.WriteLine($"found 9 at ({row}, {column}), stop");
            break outer;
        }
    }
    Console.WriteLine($"row {row}: no 0 or 9");
}
row 0: blocked at column 2, next row
row 1: no 0 or 9
found 9 at (2, 0), stop

continue outer skipped the rest of row 0 (and the “no 0 or 9” line for it), and break outer left both loops. The label goes directly on the loop. It also works for the other old annoyance, leaving a loop from inside a switch, where a plain break only leaves the switch:

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

The errors

Program.cs(4,11): error CS9393: No enclosing loop or switch statement with the label 'done' out of which to break
Program.cs(7,22): error CS9394: No enclosing loop with the label 'choice' out of which to continue
Program.cs(3,24): error CS1632: Control cannot leave the body of an anonymous method or lambda expression

CS9393 means the label isn’t on a loop or switch that contains the break. I got it both for a label on a normal statement before the loop and for a label that doesn’t exist at all. CS9394 is the continue version, and it also fires when the label is on a switch: the break message accepts a “loop or switch statement”, the continue message only a loop. CS1632 is the lambda case: break outer inside a lambda in the loop is still not allowed, because the lambda is a separate method.

IDE0410 finds the old patterns

There’s a new style rule, IDE0410, that suggests labeled jumps. In a build it only reports when you turn it on:

<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
dotnet_diagnostic.IDE0410.severity = warning
Program.cs(18,37): warning IDE0410: Use labeled jump statement (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0410)

Line 18 is the goto done; version. The same file also had the bool found flag version (with && !found in the outer loop condition), and the build didn’t flag it. The documentation says the rule covers flags too, so the shape of your flag code probably matters; don’t count on the build finding every one.

2. Collection Expression Arguments: with(…)

Collection expressions ([1, 2, 3], C# 12) couldn’t pass constructor arguments, so you couldn’t set a capacity or a comparer. Now the first element can be with(...):

List<string> plain = [.. values];
List<string> sized = [with(capacity: 100), .. values];
Console.WriteLine($"plain.Capacity = {plain.Capacity}, sized.Capacity = {sized.Capacity}");

HashSet<string> tags = [with(StringComparer.OrdinalIgnoreCase), "Hello", "HELLO", "hello"];
Console.WriteLine($"tags.Count = {tags.Count}");

HashSet<string> defaultTags = ["Hello", "HELLO", "hello"];
Console.WriteLine($"defaultTags.Count = {defaultTags.Count}");
plain.Capacity = 3, sized.Capacity = 100
tags.Count = 1
defaultTags.Count = 3

The comparer case is the useful one: before C# 15 a case-insensitive HashSet meant going back to new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ... }.

The errors

Program.cs(1,28): error CS9354: 'with(...)' element must be the first element
Program.cs(1,18): error CS9355: 'with(...)' elements are not supported for type 'int[]'
Program.cs(2,23): error CS9357: 'with(...)' element for a read-only interface must be empty if present
Program.cs(1,28): error CS1503: Argument 1: cannot convert from 'System.StringComparer' to 'System.Collections.Generic.IEnumerable<string>'

In order: with placed after other elements; with on an array, which has no constructor to call; with(capacity: 10) on an IEnumerable<int> target (the same line on IList<int> compiled fine); and a comparer passed to List<string>, which has no comparer constructor. The last one is an ordinary overload error, so the message talks about IEnumerable<string> rather than about with.

3. Extension Indexers

C# 14 added extension blocks with extension properties. C# 15 adds indexers to them, including indexers with more than one parameter:

public static class SequenceExtensions
{
    extension(IEnumerable<int> sequence)
    {
        public int this[int index] => sequence.ElementAt(index);
    }

    extension(IReadOnlyDictionary<string, string> dictionary)
    {
        public string this[string key, string fallback] => dictionary.TryGetValue(key, out var value) ? value : fallback;
    }
}
numbers[2] = 3
settings["Font", "Consolas"] = Consolas
settings["Theme", "light"] = dark

settings is a Dictionary<string, string>, and the indexer declared for IReadOnlyDictionary applied to it. One thing to know before you add an indexer to a common interface: a type’s own indexer wins. Here the extension prints a line when it runs:

Console.WriteLine(list[1]);
Console.WriteLine(seq[1]);
output: 20
output: extension indexer called
output: 20

list is a List<int> and used its own indexer; the extension ran only for the variable typed as IEnumerable<int>. That’s the same rule as for extension methods. Also note that an extension indexer on IEnumerable<T> calls ElementAt, so on a non-list sequence every index walks the sequence from the start: numbers[i] looks free but isn’t. The one error specific to indexers:

Program.cs(6,20): error CS9303: 'this[]': cannot declare instance members in an extension block with an unnamed receiver parameter

That’s extension(IEnumerable<int>) without a parameter name; indexers are instance members, so the receiver needs one.

4. Closed Hierarchies

A closed class can only be derived from in its own assembly, so the compiler knows every direct subtype and a switch over them needs no default arm:

public closed record class GateState;
public record class Closed : GateState;
public record class Open(float Percent) : GateState;

static string Describe(GateState state) => state switch
{
    Closed => "closed",
    Open(var percent) => $"{percent}% open",
};

No warning for that switch. Remove the Closed arm and you get the warning that makes the feature worth using, because it names the missing case:

Program.cs(2,42): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'Closed' is not covered.
Program.cs(2,35): error CS9381: 'Shape': a closed type cannot be sealed or static
Program.cs(1,9): error CS0144: Cannot create an instance of the abstract type or interface 'GateState'

A closed class is implicitly abstract (hence CS0144 on new GateState()) and can’t be sealed. Closed hierarchies vs union types, and when to pick which, are compared in the union types post.

5. Static Interface Members on .NET Framework

The least visible change, and useful if you maintain libraries that still target .NET Framework: a non-virtual static method in an interface now compiles for runtimes without default interface support.

public interface IMeasurement
{
    public static double ToMeters(double centimeters) => centimeters / 100;
}

Built for net48:

### net48_static_member_cs14 (net48, <LangVersion>14</LangVersion><ImplicitUsings>disable</ImplicitUsings>)
Program.cs(5,26): error CS9327: Feature 'static members in interfaces' is not available in C# 14.0. Please use language version 15.0 or greater.
### net48_static_member_cs15 (net48, <LangVersion>15</LangVersion><ImplicitUsings>disable</ImplicitUsings>)
(no errors or warnings)
output: 2.5
### net48_default_member_cs15 (net48, <LangVersion>15</LangVersion><ImplicitUsings>disable</ImplicitUsings>)
Program.cs(5,12): error CS8701: Target runtime doesn't support default interface implementation.

With C# 15 it builds and runs on .NET Framework 4.8. A default instance method in the interface (the third case) still needs runtime support, so that remains a .NET Core-only feature.

Can You Use C# 15 on .NET 10?

By default, no. The .NET 11 SDK picks C# 14 for a net10.0 project:

Program.cs(5,27): error CS9327: Feature 'labeled break and continue' is not available in C# 14.0. Please use language version 15.0 or greater.
Program.cs(8,16): error CS9327: Feature 'collection expression arguments' is not available in C# 14.0. Please use language version 15.0 or greater.

With <LangVersion>15</LangVersion> in the net10.0 project, labeled break, with(...) and the extension indexer all compiled and ran on the .NET 10.0.12 runtime:

output: break at 0,1
output: 10
output: 6

closed did not, because the compiler needs an attribute that only exists in .NET 11:

Program.cs(20,28): error CS0656: Missing compiler required member 'System.Runtime.CompilerServices.IsClosedTypeAttribute..ctor'

Microsoft supports each C# version only with its matching .NET version, so setting LangVersion higher than the default is at your own risk. The pure syntax features worked in my test; features that need types from the .NET 11 libraries don’t compile without help. For union types, the union post shows a two-type polyfill that makes them build on .NET 10; I didn’t try the same for closed. Upgrading the project to .NET 11 is the supported route; the .NET 11 upgrade post lists what breaks.

Quick Reference: C# 15 Errors

Code Cause Fix
CS9393 break label where the label isn’t on an enclosing loop/switch Put the label directly on the outer loop
CS9394 continue label to a switch or a non-enclosing label Label a loop, not the switch
CS1632 break/continue inside a lambda Return a value from the lambda and branch outside it
CS9354 with(...) not first Move it to the front
CS9355 with(...) on an array Use a List<T> or drop the arguments
CS9357 Arguments in with(...) for IEnumerable<T> Target a concrete type or an IList<T>
CS9303 Extension indexer with an unnamed receiver extension(IEnumerable<int> sequence)
CS8509 Closed hierarchy case missing in a switch Add the named case
CS9381 closed combined with sealed or static Remove one of them
CS9327 C# 15 feature with C# 14 (e.g. net10.0) Target net11.0
CS0656 closed on .NET 10 with LangVersion 15 Target net11.0

Official reference: What’s new in C# 15 on Microsoft Learn.

Related: C# 15 union types · upgrading to .NET 11 · extension methods in C# · C# 13 and C# 14 features · all C# tutorials

Similar Posts

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *