So I’ve recently dabbled with rust a bit and boy does it have a learning curve. While I was working through the incredible rust book, I came across discriminated unions. I was not familliar with this concept before (shame, I know… I wasn’t born writing Haskell like some of you, OK?) and it absolutely blew my mind.

For all of you who don’t yet know what they are, discriminated unions let you express “this value is one of these possible things” in a way that the compiler enforces exhaustive handling. I felt like I finally found something I never even knew I was missing. And then I went back to C#… I drank the cool aid man, there’s no going back!

The typical workarounds to wrench something like this in to a language are painful. Throw an exception (and catch it somewhere upstream like a barbarian), use an out parameter for the error case, return a nullable and hope for the best, or cook up some custom Result<T> class that only covers the two-case scenario. None of it feels right.

Enter OneOf, a marvel of modern engineering that gets you most of the way there, today! The idea is simple: OneOf<T0, T1, T2, ...> is a type that holds exactly one value which can be any of the specified types. You get the same exhaustive matching you’d expect from a proper union type.

Here’s what it looks like in practice:

public OneOf<User, NotFound, ValidationError> GetUser(int id)
{
    if (id <= 0) return new ValidationError("ID must be positive");
    var user = _db.Find(id);
    if (user == null) return new NotFound();
    return user;
}

Rusts syntactic sugar is missing, ofc, but using .Match actually works fine most of the time.

var result = GetUser(42);

string message = result.Match(
    user    => $"Hello, {user.Name}!",
    _       => "Nobody home.",
    error   => $"Bad input: {error.Message}"
);

If you add a fourth type to GetUser’s return signature and forget to update the .Match() call, it won’t compile. That’s the whole point. No more “oh right, I forgot the error case” bugs sneaking through at runtime".

There’s also .Switch() for when you want side effects instead of a return value, and the library ships a OneOfBase<...> you can inherit from to build your own named union types which reads a lot more cleanly at call sites.

The icing on top is OneOf.Monads, another NuGet package built on top of OneOf that provides some convenience monads like Option<T>, Result<TError, TSuccess> and so on. It’s invigorating to use. It makes error handling soooo much tidier!

If you never used this pattern for error handling, do try it. It’s fantastic!

Now I just need to convince my team lead to rewrite the whole project with monads…

Thanks for reading 🧙

//EDIT 10.04.26:

It’s Happening

Ok, ok, chill! I want you to stay calm, but THEY DID IT! Or THEY WILL DO IT!

We’re gonna get discriminated unions in C# 15 / .NET 11 🥳🥳🥳

Can’t remember the last time I was so exited about a programming language feature lol