Implementing JavaScript Promise in 70 lines of code!

17,799 reads

@ keyvan.m.sadeghi Keyvan M. Sadeghi CTO and Co-founder of Assister.Ai, we're creating a Private, Open, General Assistant Platform.

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!

reactions

First thing that you notice is that a Promise has three states, so should we:

reactions

const states = { pending : 'Pending' , resolved : 'Resolved' , rejected : 'Rejected' };

Using a class sounds reasonable since we should be able to create a

new Promise()

resolve

reject

Nancy

reactions

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); } } }

. Ah, and let’s name our class something else! It’s an object that canor. Hmm, google thinks thatis capable of those! Let’s go with that:

Now errors thrown during Promise execution like

new Nancy(resolve => { throw new Error(); })

reject

new Nancy(resolve => resolve(42))

this.state

states.resolved

this.value

42

resolve

reject

reactions

const getCallback = state => value => { this .value = value; this .state = state; }; const resolve = getCallback(states.resolved); const reject = getCallback(states.rejected);

are captured by. We can also do things likeexcept… it doesn’t do what we want!would be changed to, but we also needto be! Let’s change theanddefinitions:

We used a Higher-Order-Function,

getCallback

resolve

reject

resolve(42)

reactions

, to avoid repeated code forand. Ournow works as expected.

Time for the beefier stuff! The infamous “ then ”s. The

then

reactions

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

interface allows a Promise to be chained, which means it should return a Promise. First we create Nancy.resolve and Nancy.reject syntactic sugars:

This allows us to write our

new Nancy(resolve => resolve(42))

Nancy.resolve(42)

reactions

// 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

as. Now let’s see what we expect from then:

then

rejected

resolved

if

reactions

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)); } }

has different behaviour inandstates. That means lots of “”s, or… maybe we can do better?

As you see, no

if

changeState

reactions

! We’ve implemented a mechanism for “shifting the gear”, ourbehaves differently on each gear. Thatfunction in line 18 does what all those condition checks would do for us, voila!

One caveat:

Nancy.resolve(42).then(() => { throw new Error(); })

rejected

Nancy.try

reactions

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())); } }

. This should result in astate, but throws the error instead. Not to worry! Our friends at TC39 have a proposal that we are just going to implement. Introducing

You may think implementing

catch

then

reactions

[states.resolved]: { ... then: tryCall, catch : _ => this }, [states.rejected]: { ... then: _ => this , catch : tryCall }

is about as much hassle. Think again! It’s as easy as inverting

Now this works:

reactions

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:

reactions

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

reject

value

resolve

reject

getCallback

value

changeState

apply

reactions

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); } };

andand unpacking a Promiseon(and not). We address both these issues inby moving the previousassignment andcall to a new function,

Well, no escaping the “

if

reactions

”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)!

reactions

In order to create an async scenario, we first write the

Nancy

delay

reactions

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!

version of the popularfunction:

We should also accommodate for multiple

then

catch

reactions

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!

andon a single Promise:

The problem is, our code knows how to handle

then

catch

resolved

rejected

reactions

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(); } } }; ... } ... }

andon aorstate, 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

resolve

laterCall

apply

reactions

and returned Promise’sin a. We call these at the end oflater. Boom!

We may not be particularly proud of the verbose code of our

callLater

reactions

definition. Not to worry though, one day we will re-write it with the pipe syntax.

Here’s our code in its final glory:

reactions

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

Nancy

reactions

namedin 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!

reactions

Let me know your feedback in the comments section.

reactions

Tags