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

Why monads

Why return an Option or a Result instead of using null and exceptions.

You already have ways to say "no value" and "it failed". C# gives you null and it gives you exceptions. So why add a library?

Because neither one shows up where you read it: the method signature.

What goes wrong today

Nothing here is exotic. It is the ordinary shape of C# code.

  • A method returns null when a business rule says there is nothing to return.

  • A method throws when a business rule says the work cannot continue.

  • Callers wrap the call in try/catch just to log and re-throw.

  • Guard clauses pile up at the top of every method.

  • Branching logic spreads across if, else and switch until the happy path is hard to find.

The cost is that a signature tells you almost nothing. Ritual PrepareRitual(decimal) looks total. It might return null. It might throw. You find out in production.

What a monad actually is

The word sounds academic. In practice it is small.

A monad is a type that wraps a value and gives you one consistent way to keep working with it — including when there is no value, or when something failed.

This library ships two of them:

  • Option<T> — there might be a value.

  • Result<T, E> — this might have failed.

That is the whole idea. The rest is what you can do with it.

The same code, both ways

Here is a spell being cast. Components go in, an effect comes out, and several things can go wrong along the way.

Written the usual way:

Count what is actually happening. Two of those branches are business rules wearing an exception costume. The caller still cannot tell success from failure, and the reason for the failure was thrown away on the last line.

Now the same thing with monads:

_logger is whatever logger you already use. It is scenery in both samples — nothing in this library requires one.

No conditionals. No defensive null checks. No local variables scattered between the steps. Every failure that can happen is named in a signature, and the reason survives all the way to the caller.

The picture: two railway tracks

Think of your program as a train.

Each step of your logic is a station. The train loads data, transforms it, runs a check, and moves on. Things go wrong: a record is missing, a validation fails, a file is not there.

Without monads, a problem derails the train. An exception unwinds the stack past every station you cared about. A null slips through and derails you three stations later, somewhere unrelated. Some methods return null, some throw, some just work — so you cannot chain them without guessing.

With monads, there are two tracks. A success track, where the train keeps moving. A failure track, where it is quietly diverted to a siding.

  • 🚆 Success track — the next step runs.

  • 🚧 Failure or none track — the next step is skipped, and the train arrives carrying the reason.

The train never jumps between tracks by accident. It stays on one or the other, and every station is built to handle both.

If the character does not exist, or the spellbook has no patron, nothing crashes. The train stops safely at None, and you decide what that means later:

Where to go next

Last updated

Was this helpful?