For the complete documentation index, see llms.txt. This page is also available as Markdown.

Option<T>

Model a value that might not be there, and work with it without null checks.

Option<T> says a value might not be there. It has exactly two shapes:

  • Some<T> — there is a value.

  • None<T> — there is not.

That is the whole type. What makes it worth using is that you cannot read the value without acknowledging the second case, so the compiler catches what a null check would have let through.

Other languages call this Maybe<T>. Same idea.

Why not just use null?

null tells you nothing. It does not say why the value is missing, or whether it was ever meant to be there. It spreads guard clauses through your code, and the compiler will still happily let you dereference it.

Option<T> says the absence out loud, in the signature, where you already look.

There is a second difference that matters more than it sounds. None is not an error. When you write:

Option<Character> FindCharacter(string name);

you are not saying "this might blow up". You are saying "this might not find anything, and that is a normal outcome". If you need to know why it failed, reach for Result<T, E> instead.

Create one

Option<string> some = Option.Some("Keyleth");
Option<string> none = Option.None<string>();
Option<string> fromNullable = Option.FromNullable(sigil);
Option<string> fromTry = Option.Try(() => sigil!.Split('@')[1]);
  • Option.Some and Option.None<T> are the two you will write most.

  • Option.FromNullable takes something that might already be null — usually at the edge of your code, where you cannot control the shape.

  • Option.Try runs a function that might throw and gives you None if it does.

Transform it

You rarely want to look inside an Option. You want to keep working, and let the None case take care of itself.

Map

Map changes the value if there is one, and does nothing if there is not.

AndThen

Use AndThen when the next step also returns an Option. Map would give you an Option<Option<T>>; AndThen keeps it flat.

AndThen short-circuits. If anything in the chain is None, the later functions never run.

Filter

Filter keeps a value only if it passes your predicate. If it does not, you get None.

This is the replacement for an if-guard in the middle of a pipeline.

Chain them

Put those three together and the whole thing reads top to bottom, with no branching at all:

Compare that with the version you would otherwise write:

Same behaviour. One of them tells you what it is doing.

Get the value back out

Every chain ends somewhere. These are the ways out.

Match

Match is the honest one. You supply both branches and get a plain value.

Unwrap with a fallback

Use UnwrapOr when the fallback is already sitting there. Use UnwrapOrElse when producing it costs something — the function only runs on a None.

Wrapping a value you already hold in a lambda gets you the worst of both. The fallback is built either way, and the call allocates a delegate to defer work that has already happened. If you can write it as an argument, pass it as one.

Pattern matching

Since 7.0.0, Option<T> deconstructs, so C# pattern matching works on it directly:

And exhaustively, in a switch:

The discard arm is there because the compiler cannot see that Some and None are the only two cases. It never runs.

Check without unwrapping

Sometimes you only want a bool.

  • IsSomeAnd — there is a value and it passes the predicate.

  • IsNoneOr — there is no value, or the one there passes.

Combine two options

Zip, ZipWith and Unzip

Zip pairs two options into one, and gives None if either side is missing.

ZipWith does the same but combines the two values yourself instead of making a tuple:

Unzip reverses a Zip:

A component that happens to equal its type's default is an ordinary value here, so Option.Some((0, "x")).Unzip() gives (Some(0), Some("x")). This threw before 6.0.0.

Fallback chains

Or takes the first Some it finds. OrElse is the same, but the fallback is only built if it is needed.

The rest

Three more exist, and each is occasionally exactly what you want.

Method
What it does

And

Returns the second option, but only if the first was Some. Answers "did both arrive?"

Reduce

Merges two options of the same type. Your function runs only when both are Some; otherwise the one that exists comes back untouched.

Xor

Returns the value only if exactly one of the two is Some.

Work with a collection of them

A List<Option<T>> has its own set of helpers — Collect, Flatten, Map, Filter, FirstOrNone and friends. They live in Waystone.Monads.Options.Extensions, and are covered on the Option<T> collections reference.

To step out of a single Option and into System.Linq, use AsEnumerable. It gives you a sequence of nothing or one:

For real LINQ query syntax over an Optionfrom, where, select, staying inside the monad the whole way — see Waystone.Monads.Linq.

Printing and logging

ToString() never shows the wrapped value. You get the state and nothing else:

Option<T> is a record and Some<T> keeps its value in a private property, so the compiler-generated ToString() has nothing to print. Interpolating an option into a log message tells you whether a value was there, never what it was.

To log the value when it exists, use Inspect. It runs your action only on a Some, and hands the option back so the chain continues:

Nothing runs on a None. To log both branches, use Match.

When to reach for it

Use Option<T> when:

  • The value is intentionally optional, not missing by accident.

  • You want a chain that bails out early on absence.

  • You do not care why it is absent.

  • You want the caller to have to deal with the empty case.

Reach for something else when:

  • The default of a value type already means absence — 0 for a count, say.

  • You care about the reason. That is Result<T, E>.

Where to go next

  • Result<T, E> — the same idea, for failure.

  • Async — keeping a chain intact across an await.

  • Option<T> API — every overload, when you need one this page did not show.

Last updated

Was this helpful?