Monads, demystified: chaining operations that might fail, be absent, or be async
"Monad" has a reputation as functional programming's most intimidating word — largely because most explanations reach for abstract category theory before showing a single concrete example. This lesson does the opposite: it shows the concrete problem first, and the word itself turns out to be almost an afterthought.
Advanced
3 min read
The problem .map() alone can't solve: a function that itself returns a wrapped value
def find_user(user_id) -> "Some | NoneOption": ... # returns Some(user) or NoneOption()def find_manager(user) -> "Some | NoneOption": ... # ALSO returns Some(manager) or NoneOption()find_user(42).map(find_manager)# PROBLEM: this produces Some(Some(manager)) or Some(NoneOption()) —# a DOUBLY-WRAPPED option, not the flat Some(manager) / NoneOption() we actually want
Lesson 9's .map() works perfectly when the function being applied returns a plain value — but find_manager itself returns an Option, not a plain Manager. Mapping with it produces an Option containing another Option — a nested, doubly-wrapped mess that isn't actually useful, since now you'd need to unwrap twice instead of once.
The fix: a method that flattens the nesting automatically
class Some: def __init__(self, value): self.value = value def flat_map(self, f): return f(self.value) # f already RETURNS an Option — don't wrap it againclass NoneOption: def flat_map(self, f): return selffind_user(42).flat_map(find_manager)# Some(manager) or NoneOption() — FLAT, exactly what we wanted
flat_map (also called bind, or >>= in Haskell) is exactly .map(), except it expects the transforming function to already return a wrapped value, and it doesn't wrap the result a second time — it trusts the function's own wrapper instead of adding another layer on top. A Monad is simply any container type that provides this flat_map/bind operation (on top of the Functor's .map() from lesson 9) — that's the entire definition. Every intimidating abstraction built on top of that single fact is elaboration, not a separate, harder prerequisite.
Chaining several fallible steps cleanly, the actual payoff
find_user(42).flat_map(find_manager).flat_map(find_manager_email)# Each step can fail (return NoneOption) — and if ANY step does,# every subsequent flat_map is simply skipped automatically,# with no explicit "if it worked, then..." check needed anywhere
This is the entire practical value of monads: chaining several operations that can each independently fail or be absent, with the failure/absence handling happening automatically at each step, rather than an explicit nested if check after every single one. The moment find_manager returns NoneOption, every later flat_map in the chain simply passes that NoneOption through unchanged, and the function arguments to those later steps never even run.
Recognizing this pattern in code this curriculum has already covered
JavaScript's own Promise .then() (covered in this curriculum's own promises lesson) is a monad's flat_map in disguise: each .then() callback returning anotherPromise gets automatically flattened — .then() never produces a Promise<Promise<T>>, it resolves the inner Promise and hands you the plain result directly. Anyone who has ever chained .then() calls to sequence several async operations that could each fail has already used a monad in practice, almost certainly without ever needing the word to use it correctly.