Skip to main content

Command Palette

Search for a command to run...

Monads Part 3: Why do we need monads?

A series where I discuss how monads are at the core of Effect.

Updated
•7 min read•View as Markdown

I really like this explanation of when to use a monad from Wikipedia:

A monad can be used where unrestricted access to a value is inappropriate for reasons specific to the scenario. — https://en.wikipedia.org/wiki/Monad_(functional_programming)

This one sentence captures the essence of why monads exist in programming: sometimes you shouldn’t (or simply can’t) treat a value as if it were just sitting there, ready to use.

  • With the Maybe monad (called Option in Effect), the reason is simple: the value might not exist. If you try to use it blindly, you’ll end up with null checks scattered everywhere or risk runtime errors.

  • With the IO monad (you can think of Promise in JavaScript as an IO-like monad), the reason is timing: the value isn’t available yet. You can’t use it until some external process (like a network response) completes.

Monads give us a disciplined, composable way to work with such “not-quite-values.” They let you chain operations together as if you were working with plain values, while the monad itself takes care of the tricky parts: missing data, asynchronous results, or even controlled side effects.

That’s why monads are useful — they abstract away the boilerplate of how to manage context, and let you focus on what your program should do.

Monads as Programmable Control Flow

So why do we need monads at all? At their core, monads aren’t about abstract math — they’re about control flow. They let us describe how to move from one step to the next when values are wrapped in some kind of context: maybe absent, maybe error-prone, maybe asynchronous, maybe streaming.

Think of them as programmable semicolons. In “normal” code, ; just means “do the next thing.” But with monads, flatMap is the semicolon — and each monad defines its own rules for what “next” means:

  • With Option, “next” only happens if a value exists; otherwise, the chain stops.

  • With Either, “next” only happens if the computation succeeded; otherwise, the error is carried forward.

  • With Promise in JavaScript, “next” only happens once the async value arrives; errors skip directly to .catch.

  • With Effect, “next” only happens if the effect hasn’t failed or been interrupted, and it can also carry extra rules for timeouts, retries, and resource safety.

  • With Array or Chunk, “next” means applying the function to every element and flattening the results.

The key idea is that each monad bakes its control flow into flatMap. That’s why working with monads feels so natural once you learn the pattern: you focus on the happy path, and the monad takes care of all the branching, error handling, and sequencing rules automatically.

This is the real power of monads: they give you a way to program your control flow explicitly, rather than scattering it across if/else, try/catch, callbacks, or loops.

The unifying idea in the Effect library: using the flatMap operator both a sequencer and a gatekeeper. It decides whether to keep going, based on the rules of the monad:

  • Option stops on None.

  • Either stops on Left.

  • Effect stops on failure, interruption, or cancellation.

  • Stream stops when the stream ends or conditions are met.

Uniform Composition of Effectful Computations

They also give you a single, predictable way to compose computations that live inside a context. Instead of learning one composition style for arrays, another for options, another for errors, and yet another for async code, you always reach for the same two operators:

  • map — transform the wrapped value into another plain value, keeping it in the same context.

  • flatMap — transform the wrapped value into another wrapped value, and flatten the result back into a single layer.

This uniformity is more than just a convenience — it gives you a mental model you can apply everywhere. Once you understand how map and flatMap work, you can reason about any monadic data type in the same way.

In the Effect library, this consistency is built into the design. Every monad-like data type implements the same standard operators. That means you can learn the pattern once and use it across many domains:

  • Option — optional values (Some / None)

  • Either — success or failure with error info (Right / Left)

  • Result — outcome of running an effect (alias for Either/Exit)

  • Effect — the super monad for async, errors, resources, environments

  • Exit — completed outcome of an Effect

  • Cause — structured reasons for failure

  • Stream — sequences of values over time

  • Chunk — immutable collections (like efficient arrays)

  • Channel — bidirectional streams of values

  • Layer — resource construction and dependency injection

With this design, you don’t need to remember a different API for each abstraction. Whether you’re filtering a Chunk, short-circuiting an Option, sequencing an Either, or composing async logic in an Effect, you always apply the same mental model: map for pure transformations, flatMap for effectful ones.

👉 This is what “uniform composition” really means: one set of tools, many different scenarios.

Short-Circuiting Control Flow

If you’ve ever worked with JavaScript Promises, you’ve already seen short-circuiting in action.

Consider this example:

Promise.resolve(10)
  .then(n => {
    if (n <= 0) throw new Error("must be > 0");
    return 100 / n;
  })
  .then(result => `ok:${result}`)
  .catch(err => `failed: ${err.message}`);

Here’s what happens:

  • Each .then only runs if the previous step succeeded.

  • As soon as an error is thrown or a rejected Promise is returned, the pipeline skips the remaining steps and jumps to .catch.

This is short-circuiting: the control flow halts at the point of failure, and you don’t need nested if statements or multiple try/catch blocks.

Short-circuiting in Effect

The Effect library generalizes this idea far beyond Promises. Many of its monad-like data types encode their own short-circuiting rules, and all of them can be composed uniformly with map (pure transforms) and flatMap (effectful transforms).

  • Option → short-circuits when None. Once you hit None, no further steps run.

  • Either → short-circuits on Left. Errors stop the pipeline but preserve error info.

  • Effect → short-circuits when an effect fails, is interrupted, or throws.

  • Exit → represents the final outcome of an effect; failures halt further chaining.

  • Stream → stops producing values when the stream fails or ends.

  • Chunk → short-circuits when a mapping function produces an empty collection.

Examples

import { Option } from "effect";

const safeDivide = (n: number) =>
  n > 0 ? Option.some(100 / n) : Option.none();

const result = Option.some(10).pipe(
  Option.flatMap(safeDivide),
  Option.map(n => `ok:${n}`)
);

// None if invalid, Some("ok:10") otherwise

Why this matters

Just like Promises let you avoid deeply nested callbacks, monads in Effect let you avoid deeply nested error handling and conditionals. Each type knows how to short-circuit when things go wrong, so your code stays linear and focused on the happy path — while errors, absence, or interruptions are handled automatically.

👉 Whether you’re working with async code, optional values, structured errors, or streaming data, the short-circuiting behavior is built in — and always expressed through the same map and flatMap operators.

The Tradeoff: A Little Boilerplate for a Lot of Consistency

One thing you notice when you start using Effect is that the code feels a little more formal. You don’t just call functions directly — you wrap values in Option.some, Either.right, or Effect.succeed. You don’t always just return plain values — you often return a monadic type. And instead of a bare function call, you chain with map or flatMap.

That’s the tradeoff: a bit of extra boilerplate.

But the payoff is huge.

  • Consistent control flow patterns: You don’t have to juggle if/else for nulls, try/catch for errors, and callbacks for async. Instead, you always use the same operators: map for pure steps, flatMap for effectful ones.

  • Consistent terminology: Every data type speaks the same language. Whether you’re working with an Option, an Either, or an Effect, you know exactly how to transform and chain values.

  • Composable abstractions: Because the rules are standardized, you can combine async, errors, optional values, streaming, and resource management into a single pipeline without switching mental models.

This uniformity is what makes monadic code in Effect so powerful. The small upfront cost of boilerplate buys you clarity, predictability, and scalability in the long run. Your code may look a bit more ceremonial at first glance, but it’s far easier to reason about, teach, and maintain.