Core Functionality

Learn about the different functionality that is common to both the Option and Result types.

Introduction

While Option<T> and Result<T, E> serve different purposes (absence vs. success/failure), they share a common operational model. If you are fluent with either the Option<T> or Result<T, E> already, you're 90% of the way to mastering the other.

The core APIs defined below work the same across both types, differing only in terms of semantics.

Creation

Both Option and Result types have factory methods that allow you to create an instance of the monad in one of it's two states.

Option<string> some = Option.Some("Hello Bees!");
Option<string> none = Option.None<string>();

Use Try to safely capture potentially exception-throwing logic inside a monadic wrapper. This is useful if you want to begin a monadic chain from a method you do not have control over.

Option<User> maybeUser = Option.Try(() => GetCurrentUser());

If the GetCurrentUser call throws, the exception is caught and logged via your configured exception logger, and you get back a None<User> instance.

Transform

Transformations are the bread and butter of monadic chains. They are used to transform the value inside the monadic wrapper and perform operations on them.

Map

Map is the most common transform. Use Map to apply a transformation to the contained value if it is present or successful.

Option<string> maybeName = Option.Some("Henry Crabgrass");
Option<int> maybeLength = maybeName.Map(name => name.Length);

Map returns the same monadic wrapper type - Option<T> stays an Option, and Result<T, E> stays a Result.

More Transforms

There are additional transform methods specific to the Option<T> and Result<T, E> types.

State Checks

These are ideal for short-circuiting logic or quick guards, but avoid using them for full branching. Reach for Match when both branches matter.

Use IsOk and IsErr when you need to check the state of the Result<T, E> and don't need to access it's value just quite yet.

Result<DateTime, Error> safeParseResult = SafeParse("2025-01-01");

safeParseResult.IsOk; // true
safeParseResult.IsErr; // false

Consume

You will eventually need to escape from a monadic wrapper in order to access to a concrete value. This is where consume methods come in.

Match

Use Match to consume the monadic wrapper when you are uncertain of the wrapper's current state. It enables you to pattern match on the outcome and apply branching logic.

Option<string> maybeName = Option.Some("Travis");

int length = maybeName.Match(
    name => name.Length,
    () => 0
);

length will be 0 if maybeName is a None<string>

Unwrap

Use Unwrap to consume the monadic wrapper when you are certain the monadic wrapper holds a value, or if you want to fail loudly if it doesn't.

Avoid Unwrap unless you've validated the presence of a value upstream. It's an intentional point of failure, like First on an empty sequence. In most cases, you should reach for Match.

Option<string> maybeName = Option.Some("Lorekeeper");
string name = maybeName.Unwrap();

UnwrapOr

An alternative to Unwrap, use UnwrapOr to consume the monadic wrapper when you have a fallback value prepared for the None or Err states.

Option<string> maybeNickname = Option.None<string>();
string nickname = maybeNickname.UnwrapOr("Lautna");
//     ^? "Mate"

UnwrapOrElse

An alternative to UnwrapOr, use UnwrapOrElse to consume the monadic wrapper when you have a fallback value that requires some expensive computation.

Option<Uri> maybeAvatar = Option.None<Uri>();
Uri avatar = maybeAvatar.UnwrapOrElse(() => GenerateAvatar());
//  ^? Generated Avatar

UnwrapOrDefault

An alternative to Unwrap, use UnwrapOrDefault to consume the monadic wrapper when you are ok with default(T) as the fallback value.

Option<string> maybeName = Option.None<string>();
string? name = maybeName.UnwrapOrDefault();
//      ^? null

You can use UnwrapOrDefault to return to the world of nullable reference types. Note that reference types will have a default value of null, and value types like int will use their default value - e.g. 0

Expect

A sibling to Unwrap, but allows you to provide a meaningful error message when an exception is thrown. Use Expect to consume the monadic wrapper when you expect it to be in a Some or Ok state, and you want to fail loudly if it isn't.

This method is useful in scenarios where an absent value indicates a logic error or misuse of the API - not a runtime condition to recover from.

Option<string> maybeName = Option.Some("Greymore");
string name = maybeName.Expect("Expected a name, but got nothing.");

More Consumes

There are consume methods specific to a Result<T, E>.

Transform and Consume

MapOr

Use MapOr when you want to apply a transformation to the contained value, and you have a fallback value ready to go in case the monadic wrapper is a None or Err. If your fallback value requires expensive computation, reach for MapOrElse.

Option<string> maybeName = Option.None<string>();
int length = maybeName.MapOr(0, name => name.Length);
//  ^? 0

Unlike Map, which allows you to continue the monadic chain, MapOr will consume the monadic wrapper and return you the underlying value.

MapOrElse

Use MapOrElse when you want to apply a transformation to the contained value, consume the monadic wrapper, and you need to perform an expensive calculation to get the fallback value.

Option<User> maybeUser = Option.None<User>();

Uri avatar = maybeUser.MapOrElse(
    () => GenerateAvatar(),
    user => user.Avatar
);

Unlike Map, which allows you to continue the monadic chain, MapOrElse will consume the monadic wrapper and return you the underlying value.

Side-Effect

Side effects allow you to conditionally access the value when it is Some or Ok and run some logic against the value without having to handle the other branch.

Inspect

Use Inspect when you want to run some logic against the value inside the monadic wrapper when it is in it's Some or Ok state without transforming the value inside. The most common use case for Inspect is to inspect the value inside the wrapper and log it's value.

Reach for Map instead if you need to transform the contained value

Option<string> maybeName = Option.Some("Geladon");
maybeName.Inspect(name => Console.WriteLine(name.Length));

More Side-Effects

There are side-effects specific to a Result<T, E>.

Nesting

You will inevitably find yourself in a situation where you have a monadic wrapper nested inside of a monadic wrapper. Use the below functions to remove a level of nesting at a time.

Flatten

Sometimes you will find yourself with an Option inside an Option or a Result inside of a Result. Use Flatten to remove a level of nesting in the monadic wrapper.

Removes one level of nesting from an Option<Option<T>>

Option<Option<string>> some = Option.Some(Option.Some("Chetney"));
Option<string> result = some.Flatten();

Transpose

Sometimes you will find yourself with a Result inside an Option or an Option inside of a Result . Use Transpose to convert between them when your business logic needs it.

Result<int, string> Divide(int a, int b);
Option<int> maybeNumber = Option.Try(() => GetNumber());

Option<Result<int, string>> maybeResult = maybeNumber
    .Map(number => Divide(number, 2));
    
Result<Option<int>, string> result = maybeResult.Transpose();

Calling Transpose in this situation declares that a Result of None<int> is a valid outcome in our business rules.

Last updated

Was this helpful?