Monads

"Monad" has a fearsome reputation, but the idea is gentle and you have already met it in disguise. A monad is a design pattern for sequencing steps that each carry a little extra baggage — the possibility of failure, a list of results, an effect — while keeping the plumbing for that baggage out of your way. You write the happy path; the monad threads the baggage through automatically.

Concretely, a monad is a container type plus two operations:

That's it. Everything else is examples. Let's meet two friendly ones.

Maybe: computations that might fail

Maybe is a sum type: a value is either Just x (success, carrying x) or Nothing (failure, carrying nothing). It replaces error-prone null with something the type-checker forces you to handle. The magic is flatMap: chain steps that might fail, and the moment one returns Nothing, the rest are skipped — no cascade of if (x !== null) checks.

type Maybe<T> = { kind: "just"; value: T } | { kind: "nothing" }; const just = <T>(value: T): Maybe<T> => ({ kind: "just", value }); const nothing: Maybe<never> = { kind: "nothing" }; // flatMap: if we have a value, run f on it; if not, stay Nothing function flatMap<A, B>(m: Maybe<A>, f: (a: A) => Maybe<B>): Maybe<B> { return m.kind === "just" ? f(m.value) : nothing; } const safeDiv = (a: number, b: number): Maybe<number> => b === 0 ? nothing : just(a / b); // chain: 100 / 2, then / 5, then / 0 -> short-circuits to Nothing const good = flatMap(safeDiv(100, 2), (x) => safeDiv(x, 5)); const bad = flatMap(safeDiv(100, 2), (x) => safeDiv(x, 0)); console.log("good:", good); // { kind: "just", value: 10 } console.log("bad:", bad); // { kind: "nothing" } — failure propagated

Notice you never wrote "if the first division failed, skip the second." flatMap did the short-circuiting for you. That is the monad earning its keep: the failure plumbing is hidden inside flatMap, and your code reads like the happy path.

List: computations with many results

The very same pattern, with a different baggage: a list monad models a computation that can return many answers. Here of is "a one-element list," and flatMap runs a function that returns a list for each element, then flattens — exactly JavaScript's built-in Array.prototype.flatMap. It automatically explores every combination.

// for each roll of die A, pair it with each roll of die B: all combinations const dice = [1, 2, 3, 4, 5, 6]; const sums = dice.flatMap((a) => dice.flatMap((b) => [a + b]), ); console.log("number of outcomes:", sums.length); // 36 console.log("ways to make 7:", sums.filter((s) => s === 7).length); // 6

flatMap here is doing the nested-loop bookkeeping — "for every a, for every b" — behind one clean expression. Same two operations (of and flatMap), completely different behaviour: that reusability is exactly why the abstraction is worth naming.

Maybe, List, and the lazy world of streams feel unrelated, yet each is a container with the same two operations, and each obeys the same three monad laws that make chaining behave sensibly:

These laws are why you can build long pipelines with confidence. And they're why languages add special sugar for monads — Haskell's do-notation, and JavaScript's async/await, which is really flatMap for the Promise monad ("a value that will arrive later"). Every time you await, you're binding a monad.

Why the pattern is worth learning

A few honest cautions as you start: