February 10, 2020
Promises have been around for a while now, but have you ever wondered how do they work? I would argue that knowing how to use something is as valuable as knowing how it works internally. In this 3 part series we're going to implement HelloPromise, fully compliant with Promises/A+ spec.
If you want to get to the meat right away, you can find the implementation here.
Before I've decided to write this document, I've been lurking the web to figure out if this topic has even been touched. Unfortunately majority of the resources I've read implement Thenables, not Promises. To call something a Promise, it must be compliant with Promises/A+ spec. If you want to know how actual Promises are implemented, you're in the right place.
Before you label me with a pompous jerk card, please let me explain. I understand that these Thenables were made to show, in the most basic terms, how Promises operate. In my opinion, though, it wouldn't hurt to at least let your readers know about it. Just make it clear, be honest. It can save your reader a lot of confusion in the future.
Some of the resources I've read didn't include a working implementation, only slick snippets of disconnected code, no source included. You have to stitch this thing up like a Frankenstein monster, then test it, only to find out it doesn't even pass 2.1.2.1 spec section(the most basic one).
HelloPromise is fully compliant with the spec AND published.
See the main title of this section? I'm not going to lie to you that understanding how Promises work is a piece of cake. Hell! First exposure to Promises can be confusing even on the user end - hence so many questions on all sorts of forums.
Higher-order functions and intuition of how to use Promises are a must.
Promises are a bridge between an asynchronous and synchronous world. They allow us to compose asynchronous functions.
In the most basic terms, Promises are state machines with 3 possible states.
If the Promise is not pending, we say that the Promise is settled.
Every Promise is a Thenable object - it implements then
function. In fact, it's the only thing
that Promises/A+ spec regulates. We can implement it however we want as long as it's following the
rules described in the spec.
Obviously, we're going to implement our Promise according to Promises/A+ spec.
Because this series is kind of, sort of like a tutorial, we're going to call our
implementation HelloPromise
deriving from Hello, World type of material.
Before we begin, we have serious decisions to make. As I've mentioned Promises can
be thought of as state machines. Let's implement our HelloPromise as an object.
It must have a public then
method, but we're also going to include two static methods -
resolve
and reject
(we need them for testing). On top of that, I propose that
we use ES6 with class properties.
So summarizing:
HelloPromise will be implemented in ES6 with class properties.
HelloPromise class must have two static methods - resolve
and reject
.
HelloPromise objects must have a public
then method.
For tests, we're going to use promises-tests mocha test suite. Passing all the tests means we've just implemented a fully compliant Promise/A+ spec.
Because original repo still didn't update its mocha dependency, npm complains about some security issues - I've fixed it in my fork, which you can install with npm:
$ npm i -D GrayTable/promises-tests
Combining everything we've decided so far, we need the following packages:
// the tests=> promises-aplus-tests// to transpile our ES6 code=> @babel/core=> @babel/cli=> @babel/plugin-proposal-class-properties=> @babel/preset-env
I've already prepared a ready to use env for the purposes of this post.
$ git clone https://github.com/GrayTable/hellopromise-env
It supports two commands.
# build ES5 files from ES6 source$ npm run build
# run testsuite$ npm run test
It also includes every step of the implementation in parts
directory.
In this section we will define an outline of HelloPromise implementation.
Promise has three possible states.
// project_root/src/HelloPromise.js// Possible statesconst states = {pending: 'pending', // not settledrejected: 'rejected', // settledresolved: 'resolved', // settled}
During the implementation, we're going to check types quite a lot. We're will also have to execute some functions asynchronously. Let's create a few helper functions to make our code more readable.
// Helpersconst isFunction = value => typeof value === 'function';const isObject = value => typeof value === 'object';const isPromise = value => value instanceof HelloPromise;const runAsync = fn => setTimeout(fn, 0);
Some theory first. Promises are constructed with higher-order function called executor
.
We've already established that Promises are state machines, our initial state will be pending
.
Promises also hold some value that is being set when the Promise becomes settled.
It means that initially our value is not set, it is null.
class HelloPromise {constructor(executor) {this.value = null; // not set until Promise settlesthis.state = states.pending; // initially Promise is not settled}}
But what about our executor function? It can be defined however you like:
executor = (resolve, reject) => {// should make our Promise resolve with value 5// after one secondsetTimeout(_ => resolve(5), 1000)}
Looking at the above example, how do we achieve this behaviour? In the constructor
we can call the executor
function with internal instance methods, that
manipulate Promises state and value.
class HelloPromise {constructor(executor) {this.value = null; // not set until Promise settlesthis.state = states.pending; // initially Promise is not settledtry {executor(this._resolver, this._reject);}catch (reason) {this._reject(reason);}}}
We need some way to change our Promises state and value. Let's create a
_transition
method that handles it.
class HelloPromise {constructor(executor) { ... }_transition = (state, value) => {this.state = state;this.value = value;}}
When it comes to _transition
method there are basically only two options,
we will either set our state to resolved
and assign a value, or set our state to
rejected
and assign a value. To save us some verbosity, let's add _fulfill
and _reject
methods that abstract the state variable.
class HelloPromise {constructor(executor) { ... }_transition = (state, value) => { ... }_fulfill = value => this._transition(states.resolved, value);_reject = reason => this._transition(states.rejected, reason);}
I'm jumping ahead just a little bit here. Seeing how _resolver
method is
implemented you could think - why? Different types of values have different
resolving procedures(2.3 spec section). Our _resolver
should make a decision on which procedure to use.
For now let's ignore this requirement and say that _resolver
simply fulfills the promise
with value being passed in.
class HelloPromise {constructor(executor) { ... }_transition = (state, value) => { ... }_fulfill = value => { ... }_reject = reason => { ... }_resolver = value => this._fulfill(value)}
Promise is a Thenable object - it has public then
method. For our tests
we need to also expose resolve and reject static methods. We'll skip
then implementation for now.
But what are the static methods doing? Static resolve method returns new Promise object resolved with some value. Static reject method returns new Promise that has been rejected with some value. It allows use to write:
// hello is HelloPromise object with resolved// state and value set to 1const hello = new HelloPromise.resolve(1);
class HelloPromise {constructor(executor) { ... }_transition = (state, value) => { ... }_fulfill = value => { ... }_reject = reason => { ... }_resolver = value => { ... }then = (onFulfilled, onRejected) => null;static resolve = value => new HelloPromise(resolve => resolve(value));static reject = reason => new HelloPromise((_, reject) => reject(reason));}
We're done with the outline!
Today we've talked a little bit about what is required to call something a Promise. set up our env, and finally implemented the basic outline of HelloPromise class. In the next part we'll make it comply with 2.1 and 2.2 sections of the Promises/A+ spec.
Source code for each part of this series is available here.