Keyvan M. Sadeghi

CTO and Co-founder of Assister.Ai, we're creating a Private, Open, General Assistant Platform.

Implementing JavaScript Promise in 70 lines of code!

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:
const states = {
    pending: 'Pending',
    resolved: 'Resolved',
    rejected: 'Rejected'
};
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:
class Nancy {
    constructor(executor) {
        const resolve = () => {
            this.state = states.resolved;
        };

        const reject = () => {
            this.state = states.rejected;
        };

        this.state = states.pending;
        try {
            executor(resolve, reject);
        } catch (error) {
            reject(error);
        }
    }
}
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:
const getCallback = state => value => {
    this.value = value;
    this.state = state;
};

const resolve = getCallback(states.resolved);
const reject = getCallback(states.rejected);
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
”s. The 
then
 interface allows a Promise to be chained, which means it should return a Promise. First we create 
Nancy.resolve
 and 
Nancy.reject
 syntactic sugars:
class Nancy {
    ...
    static resolve(value) {
        return new Nancy(resolve => resolve(value));
    }

    static reject(value) {
        return new Nancy((_, reject) => reject(value));
    }
}
This allows us to write our 
new Nancy(resolve => resolve(42))
 as 
Nancy.resolve(42)
. Now let’s see what we expect from then:
// Ignore
let p = Nancy.reject(42)
    .then(() => console.log('why')) // ignored
    .then(() => console.log('you')) // ignored
    .then(() => console.log('ignoring me?!')); // ignored!
// p is a Nancy
// p.state is states.rejected
// p.value is 42

const carry = output => input => {
    console.log(input);
    return output;
};

// Chain
p = Nancy.resolve(0)
    .then(carry(1)) // logs 0
    .then(carry(2)) // logs 1
    .then(carry(3)); // logs 2
// p is a Nancy
// p.state is states.resolved
// p.value is 3
then
 has different behaviour in 
rejected
 and 
resolved
 states. That means lots of “
if
”s, or… maybe we can do better?
class Nancy {
    constructor(executor) {
        const members = {
            [states.resolved]: {
                state: states.resolved,
                // Chain mechanism
                then: onResolved => Nancy.resolve(onResolved(this.value))
            },
            [states.rejected]: {
                state: states.rejected,
                // Ignore mechanism
                then: _ => this
            },
            [states.pending]: {
                state: states.pending
            }
        };
        const changeState = state => Object.assign(this, members[state]);
        const getCallback = state => value => {
            this.value = value;
            changeState(state);
        };

        const resolve = getCallback(states.resolved);
        const reject = getCallback(states.rejected);
        changeState(states.pending);
        try {
            executor(resolve, reject);
        } catch (error) {
            reject(error);
        }
    }

    static resolve(value) {
        return new Nancy(resolve => resolve(value));
    }

    static reject(value) {
        return new Nancy((_, reject) => reject(value));
    }
}
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!
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
:
class Nancy {
    constructor(executor) {
        const tryCall = callback => Nancy.try(() => callback(this.value));
        const members = {
            [states.resolved]: {
                ...
                then: trycall
            },
            ...
        };
        ...
    }
    ...

    static try(callback) {
        return new Nancy(resolve => resolve(callback()));
    }
}
You may think implementing 
catch
 is about as much hassle. Think again! It’s as easy as inverting 
then
.
[states.resolved]: {
    ...
    then: tryCall,
    catch: _ => this
},
[states.rejected]: {
    ...
    then: _ => this,
    catch: tryCall
}
Now this works:
const anything = () => {
    throw new Error('I can be anything because I should never get called!');
};
const throwSomethingWrong = () => {
    console.log('not ignored!');
    throw new Error('Something went wrong...');
};

const p = Nancy.reject(42)
    .catch(value => value) // resolves
    .catch(anything) // ignored
    .catch(anything) // ignored
    .then(value => console.log(value)) // logs 42
    .then(throwSomethingWrong) // logs not ignored!
    .catch(throwSomethingWrong) // logs not ignored!
    .catch(() => 24); // resolves
// p is a Nancy
// p.state is states.resolved
// p.value is 24
Two other things that we should fix:
let p = new Nancy((resolve, reject) => {
    resolve(42);
    reject(24); // ignored
    resolve(); // ignored
});

p
    .then(value => Nancy.reject(value)) // rejects
    .catch(value => console.log(value)); // logs 42

p = Nancy.reject(Nancy.resolve(42));
// p.state is states.rejected
// p.value is a Nancy resolved to 42
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
:
const apply = (value, state) => {
    // Ignore subsequent calls to resolve and reject
    if (this.state === states.pending) {
        this.value = value;
        changeState(state);
    }
};

const getCallback = state => value => {
    // Unpack on resolve
    if (value instanceof Nancy && state === states.resolved) {
        value.then(value => apply(value, states.resolved));
        value.catch(value => apply(value, states.rejected));
        // Either 'then' or 'catch' will happen here, not both
        // No need for more ifs!
    } else {
        apply(value, state);
    }
};
Well, no escaping the “
if
”s this time I’m afraid… until the day that 
match
comes around!
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:
const delay = milliseconds => new Nancy(resolve => setTimeout(resolve, milliseconds));
const logThenDelay = milliseconds => total => {
    console.log(`${total / 1000.0} seconds!`);
    return delay(milliseconds)
        .then(() => total + milliseconds);
};

logThenDelay(500)(0) // logs 0 seconds!
    .then(logThenDelay(500)) // after 0.5 seconds, logs 0.5 seconds!
    .then(logThenDelay(500)) // after 1 second, logs 1 seconds!
    .then(logThenDelay(500)); // after 1.5 seconds, logs 1.5 seconds!
We should also accommodate for multiple 
then
 and 
catch
 on a single Promise:
let p = delay(500);
p.then(() => console.log('1st then!')); // after 0.5 seconds, logs 1st then!
p.then(() => console.log('2nd then!')); // after 0.5 seconds, logs 2nd then!
p.then(() => console.log('3rd then!')); // after 0.5 seconds, logs 3rd then!

p = p.then(() => Nancy.reject());
p.catch(() => console.log('1st catch!')); // after 0.5 seconds, logs 1st catch!
p.catch(() => console.log('2nd catch!')); // after 0.5 seconds, logs 2nd catch!
p.catch(() => console.log('3rd catch!')); // after 0.5 seconds, logs 3rd catch!
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:
class Nancy {
    constructor(executor) {
        ...
        const laterCalls = [];
        const callLater = getMember => callback => new Nancy(resolve => laterCalls.push(() => resolve(getMember()(callback))));
        const members = {
            ...
            [states.pending]: {
                ...
                then: callLater(() => this.then),
                catch: callLater(() => this.catch)
            }
        };
        ...
        const apply = (value, state) => {
            if (this.state === states.pending) {
            ...
                for (const laterCall of laterCalls) {
                    laterCall();
                }
            }
        };
        ...
    }
    ...
}
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
syntax.
Here’s our code in its final glory:
const states = {
    pending: 'Pending',
    resolved: 'Resolved',
    rejected: 'Rejected'
};

class Nancy {
    constructor(executor) {
        const tryCall = callback => Nancy.try(() => callback(this.value));
        const laterCalls = [];
        const callLater = getMember => callback => new Nancy(resolve => laterCalls.push(() => resolve(getMember()(callback))));
        const members = {
            [states.resolved]: {
                state: states.resolved,
                then: tryCall,
                catch: _ => this
            },
            [states.rejected]: {
                state: states.rejected,
                then: _ => this,
                catch: tryCall
            },
            [states.pending]: {
                state: states.pending,
                then: callLater(() => this.then),
                catch: callLater(() => this.catch)
            }
        };
        const changeState = state => Object.assign(this, members[state]);
        const apply = (value, state) => {
            if (this.state === states.pending) {
                this.value = value;
                changeState(state);
                for (const laterCall of laterCalls) {
                    laterCall();
                }
            }
        };

        const getCallback = state => value => {
            if (value instanceof Nancy && state === states.resolved) {
                value.then(value => apply(value, states.resolved));
                value.catch(value => apply(value, states.rejected));
            } else {
                apply(value, state);
            }
        };

        const resolve = getCallback(states.resolved);
        const reject = getCallback(states.rejected);
        changeState(states.pending);
        try {
            executor(resolve, reject);
        } catch (error) {
            reject(error);
        }
    }

    static resolve(value) {
        return new Nancy(resolve => resolve(value));
    }

    static reject(value) {
        return new Nancy((_, reject) => reject(value));
    }

    static try(callback) {
        return new Nancy(resolve => resolve(callback()));
    }
}
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
 and 
Nancy.race
, 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!
Let me know your feedback in the comments section.

Tags

More by Keyvan M. Sadeghi

Topics of interest