Explainstuff.mebeta
All concepts
Functional Programmingbeginner6 min

The Maybe-Value

A labelled box that's either holding something or clearly empty — so "nothing's here" can never sneak up and crash your program.

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.

The hidden null, used directly — boom
crash at 2am
lookup
null?
use it directly
A missing value disguised as null sails straight through to code that uses it, then crashes far from where the nothing began.
Watch out

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.

A value that might be there
maybe
lookup
Option
Some 42
None
An Option is either Some (here) or None (empty); the type makes you handle the empty case.

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.

Note

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.

One missing value short-circuits the chain
missing → skip the rest
userId
find user
get account
get balance
Some balance
None
Look up user, then account, then balance — the first None bails out early and the remaining steps are skipped, untouched.

Key takeaways

  • `null` is the classic way to represent "no value" — but it hides the absence until your code touches it and crashes with a surprise.
  • F#'s `Option` makes absence honest: a value is either `Some x` (it's here) or `None` (it's missing), right there in the type.
  • Because the type says a value *might* be missing, the compiler nudges you to handle the empty case instead of forgetting it.
  • `Option.map` transforms the value inside the box if there is one, and quietly leaves an empty box empty — no unpacking, no crashes.
  • `Option.defaultValue` supplies a fallback when the box is empty, and [pattern matching](/concepts/pattern-matching) lets you handle `Some` and `None` directly.

Keep going