Monads explained in JavaScript

August 11, 2025

1,023 words

Post contents

I dunno about you, but I'm sick of hearing people obsessed with functional programming talking about "monads".

Like, I've been an engineer for years and never understood them.

And I've seen monads explained in a bunch of different ways:

But none of them quite stuck for me as a predominantly JavaScript-focused engineer.

If I may, allow me to try my hand at this.

Pre-requisite knowledge

I generally assume that you're familiar with JavaScript and Promises. If you're not, I might suggest the following resources:

Introducing Monads in JavaScript

A monad wraps data, adds context & allows for chaining through immutable operations.

In JavaScript, there's one primary built-in API that fits the bill of a Monad perfectly: Promises.


A promise takes some data:

// In JS terms:Promise.resolve(someData);// In TS terms:Promise<SomeDataType>;

And adds some additional context, in this case "time"; a promise may resolve immediately or take some time to resolve the data.

This additional context is stored in a promise as a sort of status determining the status of the time:

status: "pending" | "fulfilled" | "rejected"

That said, we don't directly interface with status; We instead interface with this status through the promise API:

Promise.resolve(12)  // Fulfilled  .then(x => x * 2) // 24	// Rejected	.catch(console.error)

The promise API isn't a one-and-done, either. You can chain then calls together to process one behavior then another:

Promise.resolve(12)	.then(x => x * 2) // 24	.then(y => y + 2); // 26

Each .then block we create introduces a new Promise from the previous Promise in the chain:

const promise1 = Promise.resolve();const promise2 = promise1.then(x => x);console.log(promise1 === promise2); // false

This means that the immutable operation of chaining is the .then, since each new usage introduces a fresh Promise without changing the previous one.

A note on chained immutability

Astute readers may note that you're still able to mutate the inner value of a monad, like so:

Promise.resolve({current: 12}) .then(x => {     setTimeout(() => console.log({x}), 100)     return x; }) .then(y => {     y.current++;     console.log({y}) })

However, this doesn't break the definition of a monad, since the definition applies to the data container not the value being tracked.


Another ecosystem's example

In Rust, they have the Option API. This API also describes a monad, since it wraps data and adds an "existance" context.

It's either Some or None:

fn main() {    println!("{:?}", Some(1));    println!("{:?}", None::<i32>);}

Which can be chained together and implements other monad laws.


Now that we have the basics out of the way, its important to note that monads have three rules they must conform to: The associative law, the left identity law, and the right identity law.

The Associative Law


The "Associative Law" says that monads can chain through nesting:

a().then(x => b(x).then(c))

Or called sequentially:

a().then(b).then(c)

Clarifying associative behavior

When I was first learning about the associative law, I was confusing it with the order of process execution.

Take the following code:

Promise.resolve("123")  .then(x => x.split(""))  .then(x => x.reverse());

If we re-arrange this to the following:

Promise.resolve("123")  .then(x => x.reverse())  .then(x => x.split(""));

We'd instead get the error:

TypeError: x.reverse is not a function

But this is an important distinction: The associativity rule doesn't mean the functions inside the chain are swappable; it means the structure of the chain itself is.

The Left and Right Identity Laws

The "Left Identity" law states that wrapping & immediately unwrapping a monad has no effect:

const f = (x) => Promise.resolve(x);const result1 = Promise.resolve(10).then(f);const result2 = f(10);result1.then(console.log); // 20result2.then(console.log); // 20

Put another way: if you take a plain value, wrap it, and then chain it, the result is the same as just applying the function to the plain value directly.

Think of it as multiplying 1 to a number; it doesn't change the value of the number.


Likewise, the "Right Identity" law states that chaining the wrapping function over a monad doesn't change the outcome:

const originalPromise = Promise.resolve(1);// These two are equivalent:const result1 = originalPromise;const result2 = originalPromise.then(Promise.resolve);

Once again, outlined differently: if you have a monadic value and you chain it with the wrapping function, you get the same monadic value back.

Think of it as multiplying 1 to a number; it still doesn't change the value of the number.

Right identity confusion

Those deeply familiar with JavaScript may know that:

Promise.resolve(Promise.resolve(1))

Unwraps to a single Promise containing 1; This to say, multiple promises unwrap to a single level.

While this can be helpful in some circumstances, it's not a property inherent to Monads. In fact, Monads should generally not auto-flatten like this.

The "Right Identity" law does not pertain to this behavior. Instead, it speaks to the ability for a monad chain to apply itself without transforming the inner value.

Conclusion

Hopefully this has been a helpful introduction to monads in JavaScript. I plan on doing more with monads, including the "why" behind them and where you might gain benefits from using them in your own projects.

Until next time!

- Corbin C.

Subscribe to our newsletter!

Subscribe to our newsletter to get updates on new content we create, events we have coming up, and more! We'll make sure not to spam you and provide good insights to the content we have.