Imagine two parcels on your doorstep. One is a plain sealed box with no label — you have no idea if there's a gift inside or just packing air, and you only find out when you tear it open. The other has a clear window: you can see at a glance whether it's holding something or sitting empty.
Most code today uses the first kind of box for "no value" — and it has a name. The maybe-value is the second kind: a box that's honest about whether it's full or empty, before you ever reach inside.
The billion-dollar mistake
For decades, the standard way to say "there's nothing here" was null. A function that might not find an answer hands you back null, and everything looks fine — until some later line tries to actually use it. Then your program blows up with a crash, often far away from where the empty value first appeared.
The deep problem is that null is invisible. Nothing in the type tells you a value might be missing, so it's terribly easy to forget the empty case and discover it the hard way, in production, at 2am.
- nullLooks exactly like a real value — nothing in the type warns you it might be missing.
- use it directlyNo check runs first, so the moment the value is actually empty the program crashes.
The inventor of null later called it his "billion-dollar mistake." Why so costly? Because the absence is hidden: "no value" looks exactly like a real value right up until your code touches it and surprise-crashes. A whole category of bugs comes from this one quiet little nothing.
How it works
The maybe-value fixes this by making absence visible. Instead of a sealed box that might secretly be empty, you get a clearly labelled box with exactly two states. Either it's Some x — it's holding a value, and there it is — or it's None — it's plainly empty, and everyone can see that.
The magic is that this lives in the type. The moment a value might be missing, the type says so out loud, and the compiler gently insists you deal with both cases instead of forgetting one.
- OptionA value that's either Some (present) or None (absent) — no surprise nulls.
The animation above shows the idea in motion: a value travels along either as a full box (Some) or an empty one (None). Watch what happens when an operation meets an empty box — it simply skips over it and passes the emptiness along, untouched. No crash, no fuss; the nothing just flows quietly through.
In F# this box is called Option, and a function that might come up empty says so right in its return type. Here's a lookup that may or may not find a number:
// The `: int option` part is the honest label on the box
let tryFind (key: string) (table: Map<string, int>) : int option =
Map.tryFind key table
tryFind "answer" myTable // Some 42 — found it, here it is
tryFind "missing" myTable // None — clearly empty
That int option in the signature is the whole point: anyone reading it knows, before running a thing, that an answer might not come back. The absence is part of the contract, not a nasty surprise.
Working without opening the box
Here's the lovely part: you can often transform what's inside the box without unpacking it first. Option.map says "do this to the value if there is one; otherwise just leave the empty box empty." You never have to write a nervous "but what if it's missing?" check yourself.
let doubled = Option.map (fun x -> x * 2)
doubled (Some 21) // Some 42 — the value inside got doubled
doubled None // None — nothing to do, stays empty
Notice the empty case took care of itself. The doubling only happened where there was something to double, and the None sailed straight through.
Eventually you'll want a plain value back, not a box. When the box is empty, Option.defaultValue lets you supply a fallback to use instead:
let count = tryFind "visits" stats // int option
let display = Option.defaultValue 0 count // if empty, use 0
In plain words: "if there's a value, use it; if it's empty, use this default instead." Now display is always an honest int, with the missing case handled in the open rather than ignored.
When you do want to handle each case yourself, reach for pattern matching. match box with | Some x -> ... | None -> ... lets you spell out both paths explicitly — and F# will warn you if you forget one, so the empty case can't slip away unnoticed.
What makes this so calming is that the empty case stops being a landmine. With null, missing meant maybe a crash later. With a maybe-value, missing is just one of two clearly labelled states, and every operation either handles it or politely steps around it. The nothing is finally out in the open.
And once you start chaining several of these maybe-returning steps together — look up a user, then their account, then their balance, any of which might come up empty — you're really building a little pipeline that either keeps succeeding or bails out the moment something's missing. That's exactly the success-or-failure railway, where the empty (or failing) case rides a parallel track straight to the end.
- StepEach step returns a maybe-value — Some if it found something, None if not.
- NoneThe moment a step comes up empty, the chain short-circuits: every later step is skipped, no crash.