DEV Community


Posted on • Updated on

JS Promises are monadic... just not the way you'd expect

Note that these apply mostly to Promise<NonPromiseType>. Some properties do not hold due to the "unwrapping" of nested promises.

FP definition

Based on Functional Programming definitions all we need are:

  • a type constructor: Promise<T> for any T
  • a unit / type converter: Promise.resolve(value)
  • a bind / combinator: Promise#then
  • have unit be left identity of bind (i.e. unit(x) >>= f <=> f x): Promise.resolve(x).then(f) is functionally the same as f(x)
  • have unit be right identity of bind (i.e. p >>= unit <=> p): myPromise.then(Promise.resolve) is functionally the same as myPromise
  • bind is associative (i.e. ma >>= λx → (f(x) >>= g <=> (ma >>= f) >>= g): promise.then(x => f(x).then(g)) is functionally the same as promise.then(f).then(g)

Thus, we have proven <Promise, Promise.resolve, Promise#then> is a monad.

Where it blocks

JS promises have one cool trick that makes them viable instead of all the alternatives we ever had (e.g. jQuery's deferred): they unwrap themselves.

For instance: Promise.resolve(2).then(x => Promise.resolve(x * x) is a Promise<int> and not a Promise<Promise<int>>. There are two ways to look at this:

  • Nested promises cannot exist as a type
  • Nested promises are aliases to their non-nested equivalents

The problems with the proof above is as follows:

const x = Promise.resolve(2);
const f = px => px.then(x => x * 2);

f(x) !== Promise.resolve(x).then(f);
Enter fullscreen mode Exit fullscreen mode

Because Promise#then is a continuation that gets a value as input (and not a promise), we cannot hold the left identity of unit of Promises for promises: we can only hold the left identity of Promises for values. That's because Promise#then plays the role of both map and flatMap.

Obviously this is the only situation this will bite you in the ass if you consider Promises monadic, otherwise you're absolutely free to treat them as that and start rewriting non async/await code to proper clean promise-based code:

promise.then(x => {
 return fetch(x).then(f);

// <=>

Enter fullscreen mode Exit fullscreen mode

Top comments (0)