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

Result<T, E>

Return failure as a value instead of throwing it, and chain fallible work.

Result<T, E> says the work might fail. It has exactly two shapes:

  • Ok<T, E> — it worked, and here is the value.

  • Err<T, E> — it failed, and here is why.

Where Option<T> models absence, Result<T, E> models failure. The difference is the E. An Option tells you nothing arrived. A Result tells you what went wrong.

Some languages call this Either<E, T>. This library puts the success case first, because that is the one you read most.

Why not just throw?

Exceptions are control flow with a bomb strapped to it.

  • They are invisible in a signature. Reward ClaimReward(Quest) looks total.

  • They are easy to forget. Nothing makes you handle one.

  • They do not compose. You cannot chain a method that throws.

  • They are catastrophic in a chain. One throw unwinds everything above it.

You cannot tell which methods throw without reading their source. A Result puts the failure in the return type, where you were already looking.

There is a second point, and it matters more. Err is not an emergency. When you write:

Result<Character, Error> FindCharacter(string name);

you are saying "this can fail, and here is what failure looks like". That is a statement about your domain, not a panic button. Keep exceptions for the things that really are exceptional.

If you are writing try/catch just to return a fallback or log something, you wanted a Result.

Create one

Leave TErr off and it defaults to the library's own Error type:

See Errors for what Error holds and how to build one from an enum.

Neither side can hold null

TOk and TErr are both constrained notnull, and the constructors enforce it. The guard arrived in 5.5.0 — before it, an Ok could hold null, and the null surfaced later as a NullReferenceException somewhere in your own code.

A default value is fine, though. It is a real value:

Result.Try is the exception. It returns an Err for a null rather than throwing — that is the point of it.

Chain fallible steps

AndThen is the workhorse. Each step returns a Result, and the first Err short-circuits the rest.

If FindCharacter fails, GetQuest and ClaimReward never run, and the original error arrives at the caller untouched. No try/catch. No special cases.

This is what people mean by railway-oriented programming. Your computation runs on one of two tracks, and it never jumps between them by accident.

Map is the same idea when the next step cannot fail — it changes the Ok value and leaves an Err alone.

Work on the error side

This is what separates a Result from an Option, so it is worth knowing well.

MapErr

MapErr transforms the error, leaving the success value alone. Reach for it when two pieces of code disagree about the error type and you need them to chain.

The last two lines are a Map that nests, then a Flatten that undoes it. That is exactly what AndThen does in one step:

Prefer the second. The first is shown because you will meet it in code that grew one method at a time.

InspectErr

InspectErr runs a side effect on the error and hands the result back unchanged, so the chain continues. Logging is what it is for.

Inspect is the same thing on the Ok branch. Use both together and each log line runs only on its own track:

UnwrapErr and ExpectErr

These pull the error out and throw if the result was actually Ok.

ExpectErr does the same, but you supply the message:

Get the value back out

Match

Both branches, one plain value out. This is the default answer.

Unwrap with a fallback

UnwrapOrElse gets the error, so you can decide the fallback based on what went wrong. It only runs on an Err.

Pattern matching

Since 7.0.0, Result<T, E> deconstructs:

And exhaustively:

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

Check without unwrapping

  • IsOkAnd — it succeeded and the value passes the predicate.

  • IsErrAnd — it failed and the error passes the predicate.

Combine two results

And and Or take a result you already have. AndThen and OrElse take a function, so the second result is only built when it is needed.

And gives you the first Err, or the last Ok:

Left
Right
Output

Ok1

Ok2

Ok2

Ok

Err

Err

Err

Ok

Err

Err1

Err2

Err1

Or gives you the first Ok, or the last Err:

Left
Right
Output

Ok1

Ok2

Ok1

Ok

Err

Ok

Err

Ok

Ok

Err1

Err2

Err2

OrElse is how you recover. Its function runs on the error, not the value:

Work with a collection of them

A sequence of Result has its own helpers — Collect, Partition, Flatten, FlattenErr and AsEnumerable. They live in Waystone.Monads.Results.Extensions, and are covered on the Result<T, E> collections reference.

The short version:

  • Collect — all or nothing. One Err fails the whole batch.

  • Partition — keeps both sides, so you learn which ones failed.

For LINQ query syntax over a Result, see Waystone.Monads.Linq.

Printing and logging

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

Result<TOk, TErr> is a record and both sides keep their value in a private property, so the compiler-generated ToString() has nothing to print. Interpolating a result into a log message tells you which branch you are on, never what it holds. Use the Inspect / InspectErr pair above instead.

When to reach for it

Use Result<T, E> when:

  • A function can fail and you want that visible in the signature.

  • You care why it failed.

  • You want the caller to handle the failure explicitly.

  • You are parsing, validating, or transforming input you do not control.

  • You want exceptions to mean something has genuinely gone wrong.

Reach for Option<T> instead when you do not care about the reason.

Where to go next

  • Option<T> — the same idea, for absence.

  • Errors — the Error type, error codes, and building one from an exception.

  • Exceptions — what the library throws, and when.

  • Async — keeping a chain intact across an await.

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

Last updated

Was this helpful?