Union Types Are Finally Coming to C#

Union Types Are Finally Coming to C#

4 min read··

With Coder Agents, you can run tasks in a system you already trust. No API keys, no agent harnesses, and no special software, delegate development and research within a single control plane. Learn more here.

Sonar's acquisition of Gitar will allow the company to deliver code review from the moment an agent starts writing code to the moment it lands in the codebase. Gitar adds an AI-native code review layer on top of SonarQube's multilayered code verification platform—flagging issues, generating the fix, validating it against the CI, and committing to the branch only when the build passes. The result: fewer production surprises from AI-generated code that technically passed inspection.

Every backend developer eventually hits the same wall: a method that can return one of several things.

A parse that either gives you a number or an error. A lookup that returns a value or "not found". An operation that succeeds or fails. In C#, we've never had a clean way to model "this is an A or a B". So we faked it - with marker interfaces, abstract base classes, tuples, nullable returns, exceptions, or the excellent OneOf library.

C# 15 (shipping with .NET 11) finally adds union types to the language. I've wanted this for years, so let me give you a quick tour.

Let's dive in.

The Problem

Say a method can return a user or fail because they don't exist. Today you'd reach for something like this:

// Throw for the "failure" case - control flow via exceptions
public User GetUser(int id) =>
    _users.TryGetValue(id, out var user)
        ? user
        : throw new UserNotFoundException(id);

The signature says it returns a User, but that's a lie - it might throw instead. The caller has no way to know that without reading the body. The other usual workarounds (a bool TryGet with an out parameter, a custom Result class with nullable fields, or a OneOf<User, NotFound>) all add ceremony to express one simple idea.

What you actually want is a closed set of types. That's exactly what a union is.

Declaring a Union

The syntax is delightfully small. You list a name and the case types:

public union Result<T>(T, Exception);

That's it. A Result<T> is now either a T or an Exception - and nothing else. The types don't even need to be related, which is the whole point.

Here's a more concrete example with unrelated record types:

public record CreditCard(string Last4, string Brand);
public record PayPal(string Email);
public record BankTransfer(string Iban);

public union PaymentMethod(CreditCard, PayPal, BankTransfer);

Creating Values

There's an implicit conversion from each case type, so you just assign the value directly:

PaymentMethod method = new CreditCard("4242", "Visa");

Try to assign a type that isn't in the set, and it's a compile error. The set is closed.

Consuming a Union

This is where it shines. Pattern matching just works, and the compiler checks the inner value for you:

string Describe(PaymentMethod method) => method switch
{
    CreditCard card  => $"{card.Brand} ending {card.Last4}",
    PayPal paypal    => $"PayPal ({paypal.Email})",
    BankTransfer ach => $"Bank transfer to {ach.Iban}",
}; // No `_` or `default` needed

Notice there's no discard _ and no default arm. Because the union is closed, the compiler knows all three cases are covered. Forget one, and you get a warning at compile time:

warning CS8509: The switch expression does not handle all possible values
of its input type (it is not exhaustive). For example, the pattern 'BankTransfer'
is not covered.

That exhaustiveness check is the feature I'm most excited about. Add a new case to the union later, and the compiler points you at every switch you forgot to update.

Back to The Problem

Remember our lying GetUser method from earlier? Let's fix it with a union.

First, declare what the method can actually return - a User or a NotFound:

public record NotFound(int Id);

public union UserResult(User, NotFound);

Now the signature tells the truth, and there are no exceptions for control flow:

public UserResult GetUser(int id) =>
    _users.TryGetValue(id, out var user)
        ? user
        : new NotFound(id);

And the caller has to handle both outcomes - the compiler won't let them forget:

IResult response = GetUser(42) switch
{
    User user      => Results.Ok(user),
    NotFound found => Results.NotFound($"No user with id {found.Id}"),
};

That's the whole pitch. The return type tells you the truth: here are exactly the shapes you'll get back, and you can't ignore one by accident. No more reading the method body to discover what it might throw.

A Few Caveats

This is still a preview/experimental feature. A few things to keep in mind:

  • It targets C# 15 / .NET 11, and the syntax may still change before release. Try it on .NET 11 Preview 4 or later.
  • Under the hood, a union is compiled to a struct that boxes value-type cases and stores the contents as a single object?. There's a non-boxing path for performance-sensitive code, but the default is simple.
  • This is a type union (an A or a B), not a full discriminated union with named cases yet. It covers the vast majority of what I reach for OneOf for today.

Summary

Union types close a gap that's been open in C# for a very long time.

  • Declare a closed set of types with public union Name(A, B, C);.
  • Assign case values directly - implicit conversions handle the rest.
  • Pattern match with full compiler-checked exhaustiveness, no default arm required.
  • Model results, options, and "one of these" returns without marker interfaces, base classes, or extra libraries.

It's a small syntax with a big payoff: your method signatures finally tell the truth about what they return, and the compiler keeps every switch honest.

I'll explore this feature more in the future, but I wanted to share this quick tour now that it's available in preview.

Thanks for reading.

And stay awesome!


Loading comments...

Whenever you're ready, there are 4 ways I can help you:

  1. Pragmatic Clean Architecture: Join 5,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  2. Modular Monolith Architecture: Join 2,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  3. Pragmatic REST APIs: Join 1,900+ students in this course that will teach you how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  4. Patreon Community: Join a community of 5,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

Become a Better .NET Software Engineer

Join 74,000+ engineers who are improving their skills every Saturday morning.