Ever wondered how JavaScript Promise works internally? Obviously there is some native browser support involved, right? Nope! We can implement the Promise interface using pure JavaScript, examples are libraries like Bluebird or Q. And it’s much simpler than you may think, we can do so in only 70 lines of code! This will help with gaining a deeper insight into Promises by demystifying the underlying formation. Can also serve as a good interview question, if you are an evil employer (don’t be!). Let’s dig into it!
First thing that you notice is that a Promise has three states, so should we:
MAKE new Promise() GREAT AGAIN!
Using a class sounds reasonable since we should be able to create a new Promise()
. Ah, and let’s name our class something else! It’s an object that can resolve
or reject
. Hmm, google thinks that Nancy
is capable of those! Let’s go with that:
Now errors thrown during Promise execution like new Nancy(resolve => { throw new Error(); })
are captured by reject
. We can also do things like new Nancy(resolve => resolve(42))
except… it doesn’t do what we want! this.state
would be changed to states.resolved
, but we also need this.value
to be 42
! Let’s change the resolve
and reject
definitions:
We used a Higher-Order-Function, getCallback
, to avoid repeated code for resolve
and reject
. Our resolve(42)
now works as expected.
Time for the beefier stuff! The infamous “[then](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then)
”s. The then
interface allows a Promise to be chained, which means it should return a Promise. First we create [Nancy.resolve](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve)
and [Nancy.reject](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject)
syntactic sugars:
This allows us to write our new Nancy(resolve => resolve(42))
as Nancy.resolve(42)
. Now let’s see what we expect from then
:
then
has different behaviour in rejected
and resolved
states. That means lots of “if
”s, or… maybe we can do better?
As you see, no if
! We’ve implemented a mechanism for “shifting the gear”, our state machine behaves differently on each gear. That changeState
function in line 18 does what all those condition checks would do for us, voila!
Promise, the “State Machine”
One caveat: Nancy.resolve(42).then(() => { throw new Error(); })
. This should result in a rejected
state, but throws the error instead. Not to worry! Our friends at TC39 have a proposal that we are just going to implement. Introducing Nancy.try
:
You may think implementing catch
is about as much hassle. Think again! It’s as easy as inverting then
.
Now this works:
Two other things that we should fix:
Ignoring subsequent calls to resolve
and reject
and unpacking a Promise value
on resolve
(and not reject
). We address both these issues in getCallback
by moving the previous value
assignment and changeState
call to a new function, apply
:
Well, no escaping the “if
”s this time I’m afraid… until the day that [match](https://github.com/tc39/proposal-pattern-matching)
comes around!
OMG Async!
It’s probably time to acknowledge the elephant in the room. Where’s async in all this? Right, maybe you think it’s going to be a lot of work? Save for a good laugh (spoiler: we are 7 lines away)!
In order to create an async scenario, we first write the Nancy
version of the popular delay
function:
We should also accommodate for multiple then
and catch
on a single Promise:
The problem is, our code knows how to handle then
and catch
on a resolved
or rejected
state, we just need to hold up until the state arrives there. Our bigger problem is that we need to return a Promise right now! Hmm, well, those are not really problems, they are actually the solution! Let’s do what we just said:
We cashed both the call to then
/catch
and returned Promise’s resolve
in a laterCall
. We call these at the end of apply
later. Boom!
We may not be particularly proud of the verbose code of our callLater
definition. Not to worry though, one day we will re-write it with the [pipe](https://github.com/tc39/proposal-pipeline-operator)
syntax.
logThenDelay scenario
Here’s our code in its final glory:
Hey, we did it! A functional Promise
named Nancy
in exactly 70 lines of clean code. Hooray!
Another good exercise is to implement [Nancy.all](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)
and [Nancy.race](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all)
, but I leave that to the beloved reader. You can find the code for this article in this repository. Hope it‘s been an interesting read!