Theory

Monads

The pattern that threads context through a chain — and deletes the boilerplate

A monad is a design pattern for chaining computations that carry a context — optionality, errors, state, lists, or I/O — by threading the context through a bind operation so each step stays pure and boilerplate disappears.

  • Core operationsof + bind
  • Laws3 (identity ×2, assoc)
  • bind typeM<A> → (A→M<B>) → M<B>
  • OriginMoggi 1989, Wadler 1992
  • Generalizesfunctor → applicative → monad

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

The intuition: a value in a box you can't open carelessly

Some values arrive wrapped in a context. A database lookup might return a user, or nothing. A parse might return a number, or an error message. A random draw produces one of several possible values. An I/O call produces a value plus a side effect on the world. In each case the bare type — User, number — is wrapped in a box that records the extra story: Maybe<User>, Either<Err, number>, List<T>, IO<T>.

The problem is chaining. You want to look up a user, then their order, then the shipping address — but each step might return nothing, and each step needs the result of the last. Without help you write a staircase of if (x == null) return null; checks, or nested try/catch blocks, or hand-flattened loops. That is the boilerplate a monad deletes.

A monad is a box type that knows two things: how to put a plain value into the box (of, also called unit, return, or pure), and how to bind a function across the box (bind, also called >>=, flatMap, then, or SelectMany). The function you bind takes the unwrapped value and returns a new box. bind is responsible for opening the box, running the function, and re-threading the context so you never touch it by hand.

The precise mechanism

For a type constructor M, a monad is the pair of functions:

of   : A           → M<A>
bind : M<A> → (A → M<B>) → M<B>

The signature of bind is the whole idea. You start with a value already in the context (M<A>). You supply a function that, given a plain A, produces another value in the context (A → M<B>). And bind hands you back a single M<B> — the contexts have been merged, not nested. That merge is exactly the join / flatten step: a naive map of an A → M<B> function would give you M<M<B>>, and bind is precisely map followed by join.

What "merging the context" means is defined per monad:

  • Maybe / Optional. If the box is None, skip the function entirely and stay None. Otherwise unwrap, call the function, return its box. This is automatic short-circuiting.
  • Either / Result. A Left e (error) propagates untouched; a Right a feeds the function. The first error wins and the rest of the chain is skipped.
  • List. Apply the function to every element, then concatenate all the resulting lists — this is the Cartesian-product / non-determinism interpretation.
  • State. Thread an explicit state value through each step so it looks like mutation but stays pure.
  • IO / Promise. Sequence effects: run the first effect, take its result, run the next, preserving order in a pure description.

Performance: of and bind are O(1) wrappers around one allocation and one function call for Maybe/Either/State/IO, so a chain of n binds is O(n) in time and allocations. The List monad is the exception — its bind does a flat-map, so chaining k steps each producing b options is O(bk) values, the cost of explicit non-determinism.

The three laws

A type with of and bind is only a lawful monad if it satisfies three equations. They are not bureaucratic; they are what make refactoring and do-notation safe.

LawEquationWhat it guarantees
Left identityof(a).bind(f) ≡ f(a)of adds no extra context — wrapping then immediately binding is the same as just calling f.
Right identitym.bind(of) ≡ mBinding the trivial wrapper is a no-op — of is a true identity for the chain.
Associativitym.bind(f).bind(g) ≡ m.bind(x => f(x).bind(g))How you group consecutive binds doesn't change the result — so you can extract sub-chains into helpers freely.

Associativity is the load-bearing one. It is why a 20-line do-block can be split into three helper functions without changing behavior, and why a compiler can desugar do / async-await / LINQ query syntax into nested binds and trust the result.

When to reach for a monad — and when not to

  • Reach for Maybe/Either when a pipeline has many steps that can each fail or be absent and you want one short-circuit instead of a staircase of guards.
  • Reach for State/Reader/Writer when you want the ergonomics of mutable state, configuration, or a log without actual mutation — useful for testability and reproducibility.
  • Reach for the List monad when modeling genuine non-determinism: every combination of choices, constraint search, or generators.
  • Reach for IO/Task/Promise when you must sequence effects in order while keeping the rest of the program pure and describable.

Don't reach for one when the steps are independent — if step B doesn't need step A's value, an applicative is the right (weaker, more parallelizable) tool, because bind's sequential dependency forbids running them concurrently. And in a language without higher-kinded types or do-notation (plain Java, Go), the manual flatMap chains can be noisier than an early-return guard. A monad pays off when the context is pervasive, not occasional.

Functor vs applicative vs monad (and friends)

FunctorApplicativeMonadPromise (JS)
Core opmapap + ofbind + ofthen
Op signature(A→B) → M<A> → M<B>M<A→B> → M<A> → M<B>M<A> → (A→M<B>) → M<B>(A→B|M<B>) → M<B>
Next step can depend on prior value?NoNoYesYes
Independent steps run in parallel?n/aYesNo (sequential)No (sequential)
Auto-flattens nesting?NoNoYes (via join)Yes (and a plain value too)
Lawful monad?YesNo (then collapses M<M<T>>)
Examplesany container's mapform validation, ZipListMaybe, Either, List, State, IOasync/await sugar

The hierarchy is strict: every monad is an applicative, every applicative is a functor. The extra power of bind over ap is precisely that a later step may inspect an earlier result — which is also why monadic code is sequential and applicative code can be batched. Choosing the weakest abstraction that does the job (applicative when steps are independent) is a real engineering decision, not pedantry.

What the numbers actually say

  • Three laws, two operations. The entire interface is of and bind; everything else (map, join, ap, sequence) is derivable. map(f) = bind(x => of(f(x))) and join(mm) = mm.bind(x => x).
  • Cost per bind: O(1). One allocation + one closure call for Maybe/Either/State/IO. A pipeline of n steps is O(n) — the same as the equivalent hand-written null checks, with the branch logic written once instead of n times.
  • List bind is O(bk). k steps each branching b ways enumerates the full product — chaining 4 steps of 10 options each materializes 10,000 results. That's the price of the non-determinism it models, not overhead.
  • Boilerplate removed scales with chain length. A 6-step Maybe pipeline replaces 6 explicit null guards (12+ lines) with 6 binds and one definition. The savings are linear in the number of fallible steps.
  • History. Eugenio Moggi introduced monads to structure programming-language semantics in 1989; Philip Wadler popularized them for functional programming in 1990–1992; Haskell adopted do-notation in 1996. C#'s LINQ (2007), Scala's for-comprehensions, and JavaScript's async/await (ES2017) are all monad desugarings.

JavaScript implementation

A Maybe monad with of, bind, and a derived map. The chain short-circuits the instant any step returns None.

class Maybe {
  constructor(value, isSome) { this._v = value; this._some = isSome; }

  static of(value) { return new Maybe(value, true); }   // unit / pure
  static none()    { return new Maybe(null, false); }

  // bind / flatMap / >>=  :  Maybe<A> → (A → Maybe<B>) → Maybe<B>
  bind(f) { return this._some ? f(this._v) : this; }    // short-circuit on None

  // map is derivable from bind + of
  map(f) { return this.bind(x => Maybe.of(f(x))); }

  getOr(fallback) { return this._some ? this._v : fallback; }
}

// Domain functions return a *box*, not a bare value — that's what makes them bindable
const users = { 7: { id: 7, orderId: 42 } };
const orders = { 42: { id: 42, addressId: 99 } };
const addresses = { 99: { city: 'Oslo' } };

const findUser    = id  => id in users     ? Maybe.of(users[id])      : Maybe.none();
const findOrder   = u   => u.orderId in orders   ? Maybe.of(orders[u.orderId])     : Maybe.none();
const findAddress = o   => o.addressId in addresses ? Maybe.of(addresses[o.addressId]) : Maybe.none();

// No null checks at the call site — bind threads the "maybe-absent" context for us
const city = findUser(7)
  .bind(findOrder)
  .bind(findAddress)
  .map(addr => addr.city)
  .getOr('unknown');

console.log(city);            // "Oslo"
console.log(findUser(404).bind(findOrder).bind(findAddress).getOr('unknown')); // "unknown"

Two details worth flagging. First, the domain functions return Maybe<…>, not raw objects — that is the A → M<B> shape bind requires. Second, the entire null-propagation logic lives in one line of bind; the call site reads as a clean happy-path pipeline. Swap Maybe for an Either that carries an error message and the same code now propagates the reason for failure with zero call-site changes.

Python implementation

The same Maybe, plus a tiny State monad showing how mutation-free state threading works. Python has no do-notation, so the chaining is explicit.

from dataclasses import dataclass
from typing import Callable, Generic, TypeVar, Optional, Tuple

A = TypeVar("A"); B = TypeVar("B"); S = TypeVar("S")

# ---- Maybe monad ----
@dataclass
class Maybe(Generic[A]):
    value: Optional[A]
    some: bool

    @staticmethod
    def of(x: A) -> "Maybe[A]":   return Maybe(x, True)      # unit
    @staticmethod
    def none() -> "Maybe":        return Maybe(None, False)

    def bind(self, f: Callable[[A], "Maybe[B]"]) -> "Maybe[B]":
        return f(self.value) if self.some else self          # short-circuit
    def map(self, f): return self.bind(lambda x: Maybe.of(f(x)))
    def get_or(self, fallback): return self.value if self.some else fallback

def safe_div(a, b):  return Maybe.of(a / b) if b != 0 else Maybe.none()
def safe_sqrt(x):    return Maybe.of(x ** 0.5) if x >= 0 else Maybe.none()

print(Maybe.of(50).bind(lambda x: safe_div(x, 2)).bind(safe_sqrt).get_or("err"))  # 5.0
print(Maybe.of(50).bind(lambda x: safe_div(x, 0)).bind(safe_sqrt).get_or("err"))  # "err" — skipped

# ---- State monad: thread state purely ----
@dataclass
class State(Generic[S, A]):
    run: Callable[[S], Tuple[A, S]]      # s -> (value, new_state)

    @staticmethod
    def of(a: A) -> "State":   return State(lambda s: (a, s))
    def bind(self, f: Callable[[A], "State"]) -> "State":
        def run(s):
            a, s1 = self.run(s)          # run me, get value + threaded state
            return f(a).run(s1)          # feed both into the next step
        return State(run)

def push(x):  return State(lambda stk: (None, stk + [x]))
def pop():    return State(lambda stk: (stk[-1], stk[:-1]))

program = push(1).bind(lambda _: push(2)).bind(lambda _: pop())
value, final_stack = program.run([])
print(value, final_stack)               # 2 [1]  — no mutable variable anywhere

Note the asymmetry between the two: Maybe's context is "presence", so bind branches on a boolean. State's context is "a stack threaded through every call", so bind runs the first computation, captures the new state, and feeds it forward. Both satisfy the same three laws despite carrying completely different baggage — that uniformity is the entire payoff of naming the pattern.

Variants worth knowing

Reader, Writer, State. The three "effect" monads. Reader threads a read-only environment (dependency injection without globals); Writer accumulates a log alongside the value (a monoid append on every bind); State threads a mutable-looking value purely.

Either / Result / Validation. Like Maybe but carries why it failed. The subtle twist: Validation is deliberately applicative, not monadic, so it can accumulate all errors (e.g. every bad form field) instead of stopping at the first — a case where the weaker abstraction is the more useful one.

Continuation monad. Captures "the rest of the computation" as a function. It's the monad that can express call/cc, generators, and async control flow; it's also famously the one most people find hardest to read.

Free monad. Separates the description of a program from its interpretation. You build an abstract syntax tree of effects with bind, then run it with one interpreter for production and another for tests — no mocking framework required.

Monad transformers. Monads don't compose automatically: stacking Maybe inside State doesn't give you a working monad for free. Transformers (StateT, MaybeT, ExceptT) glue layers together so you can have, say, "state + possible failure + I/O" in one chain. The downside is the layering boilerplate, which is why algebraic-effects systems are an active alternative.

Common bugs and edge cases

  • Returning a bare value from a bind function. bind expects A → M<B>. Returning A instead of M<A> either type-errors or, in dynamic languages, produces a double-wrapped or broken box. Use map for plain functions, bind for box-returning ones.
  • Confusing map with bind / flatMap. map(f) where f returns a box gives you M<M<B>> — the classic Optional<Optional<T>> or Array<Array<T>> bug. You wanted flatMap.
  • Assuming Promise is a lawful monad. then auto-flattens and treats plain values like resolved promises, so left identity fails for a value that is itself a thenable. Fine in practice for async code, but it bites if you write generic monadic combinators against it.
  • Over-stacking transformers. A five-layer transformer stack is correct but unreadable and slow (each lift adds indirection). Flatten to one custom monad or reach for an effects system before the stack grows tall.
  • Using a monad where an applicative would parallelize. Sequencing independent network calls with bind forces them serial; the applicative ap (or Promise.all) lets them overlap. bind's dependency is sometimes a cost, not a feature.
  • Forgetting short-circuit semantics. Side effects placed after a step that returns None/Left never run. That's the point — but it surprises people who expect every line to execute.

Frequently asked questions

What is a monad in one sentence?

A monad is a type with two operations — of (wrap a plain value into the context) and bind (feed the inner value into a function that returns a new wrapped value) — that together let you chain context-carrying computations without unwrapping by hand. The context might be may-be-absent, may-have-failed, stateful, non-deterministic, or effectful.

What is the difference between a functor, an applicative, and a monad?

A functor gives you map: apply a normal function a → b inside the context. An applicative adds ap: apply a wrapped function F(a → b) to a wrapped value, letting you combine independent contexts. A monad adds bind: apply a function a → F(b) whose own result is wrapped, so each step can depend on the previous one's value. Every monad is an applicative, and every applicative is a functor.

What are the three monad laws?

Left identity: of(a).bind(f) equals f(a). Right identity: m.bind(of) equals m. Associativity: m.bind(f).bind(g) equals m.bind(x => f(x).bind(g)). They guarantee that of injects without side effects and that the order you group binds doesn't change the result, which is what makes do-notation and refactoring safe.

Is JavaScript's Promise a monad?

Almost, but no. Promise.then behaves like bind when the callback returns a promise, but then auto-flattens nested promises and treats a plain value the same as a resolved one, so Promise<Promise<T>> can't exist. That collapsing breaks the type Monad(Monad(T)) the laws assume, so Promise is monad-shaped rather than a lawful monad. Arrays with flatMap and Optional/Maybe types are cleaner examples.

Why do monads help with null checks and error handling?

Maybe and Either move the if (x == null) return null and try/catch out of every call site and into a single bind definition. bind short-circuits automatically: the first None or Left skips the rest of the chain. You write the happy path once and the propagation is handled by the type, eliminating the nested-pyramid boilerplate.

Do I need category theory to use monads?

No. The name comes from category theory, but using one only requires understanding of (wrap) plus bind and the three laws. The category-theoretic monad (an endofunctor with unit and join natural transformations) is the same structure under a different vocabulary; you can write Maybe and chain calls without ever opening a category theory book.