How Javascript promises actually work inside out

One of the most important questions I faced in the interviews how promises are implemented since async/wait is becoming more popular so you need to understand promises to master async/wait.

What is a Promise?

A promise is an object which represents the result of an asynchronous operation which is either resolved or rejected(with a reason)

There are 3 states

Fulfilled: onFulfilled() will be called (e.g., resolve() was called)

will be called (e.g., was called) Rejected: onRejected() will be called (e.g., reject() was called)

will be called (e.g., was called) Pending: not yet fulfilled or rejected

So let’s see how’s it been implemented internally:

https://github.com/then/promise/blob/master/src/core.js

According to definition at mozilla: It takes executor function as an argument

function noop() {} function Promise(executor) { if (typeof this !== 'object') { throw new TypeError('Promises must be constructed via new'); } if (typeof executor !== 'function') { throw new TypeError('Promise constructor\'s argument is not a function'); } this._deferredState = 0; this._state = 0; this._value = null; this._deferreds = null; if (executor === noop) return; doResolve(executor, this); }

Looks like simple function with some properties initialised to 0 or null but few things to notice

this._state property can have three possible values as described above

0 - pending 1 - fulfilled with _value 2 - rejected with _value 3 - adopted the state of another promise, _value

So initially its 0 (pending) when you create new a promise

Later doResolve(executor, this) is invoked with executor and promise object

Let’s move on to the definition of doResolve and see how’s its implemented:

/** * Take a potentially misbehaving resolver function and make sure * onFulfilled and onRejected are only called once. * * Makes no guarantees about asynchrony. */ function doResolve(fn, promise) { var done = false; var resolveCallback = function(value) { if (done) return; done = true; resolve(promise, value); }; var rejectCallback = function(reason) { if (done) return; done = true; reject(promise, reason); }; var res = tryCallTwo(fn, resolveCallback, rejectCallback); if (!done && res === IS_ERROR) { done = true; reject(promise, LAST_ERROR); } }

Here it is again calling tryCallTwo function with executor and 2 callbacks which is again calling resolve and reject

done variable is used here to make sure promise is resolved or reject only once so if you try to reject or resolve a promise more than once than it will just return because done = true

function tryCallTwo(fn, a, b) { try { fn(a, b); } catch (ex) { LAST_ERROR = ex; return IS_ERROR; } }

This function indirectly calling main executor callback with 2 arguments which contains logic how resolve or reject is being called. You can check resolveCallback and rejectCallback in doResolve function above.

If there is an error during execution it will store error in LAST_ERROR and return that error

Before we jump to resolve function definition, let’s check .then function first:

Promise.prototype.then = function(onFulfilled, onRejected) { if (this.constructor !== Promise) { return safeThen(this, onFulfilled, onRejected); } var res = new Promise(noop); handle(this, new Handler(onFulfilled, onRejected, res)); return res; }; function Handler(onFulfilled, onRejected, promise) { this.onFulfilled = typeof onFulfilled === "function" ? onFulfilled : null; this.onRejected = typeof onRejected === "function" ? onRejected : null; this.promise = promise; }

So in the above function then is creating new promise and assigning it as property to a new function called Handler with onFulfilled & onRejected and later it will use this promise to resolve or reject with value/reason.

As you can notice .then function is calling again another function

handle(this, new Handler(onFulfilled, onRejected, res));

Implementation:

function handle(self, deferred) { while (self._state === 3) { self = self._value; } if (Promise._onHandle) { Promise._onHandle(self); } if (self._state === 0) { if (self._deferredState === 0) { self._deferredState = 1; self._deferreds = deferred; return; } if (self._deferredState === 1) { self._deferredState = 2; self._deferreds = [self._deferreds, deferred]; return; } self._deferreds.push(deferred); return; } handleResolved(self, deferred); }

There is a while loop which will keep assigning resolved promise object to the current promise which is also a promise for _state === 3

If _state = 0(pending) and promise state is been deferred until another nested promise is resolved and its callback is stored in self._deferreds

function handleResolved(self, deferred) { asap(function() { // asap is external lib used to execute cb immediately var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected; if (cb === null) { if (self._state === 1) { resolve(deferred.promise, self._value); } else { reject(deferred.promise, self._value); } return; } var ret = tryCallOne(cb, self._value); if (ret === IS_ERROR) { reject(deferred.promise, LAST_ERROR); } else { resolve(deferred.promise, ret); } }); }

What’s happening:

If the state is 1(fulfilled) then call the resolve else reject

the resolve else reject If onFulfilled or onRejected is null for example if we used empty .then() resolved or reject will be called respectively

or is for example if we used empty resolved or reject will be called respectively If cb is not empty then it is calling another function tryCallOne(cb, self._value)

function tryCallOne(fn, a) { try { return fn(a); } catch (ex) { LAST_ERROR = ex; return IS_ERROR; } }

tryCallOne : This function not doing much other than calling the callback that has been passed with argument self._value so if there is no error it will resolve the promise otherwise reject

Every promise must supply a .then() method with the following signature:

promise.then( onFulfilled?: Function, onRejected?: Function ) => Promise

Both onFulfilled() and onRejected() are optional.

optional. If the arguments supplied are not functions, they must be ignored.

onFulfilled() will be called after the promise is fulfilled, with the promise’s value as the first argument.

will be called after the promise is fulfilled, with the promise’s value as the first argument. onRejected() will be called after the promise is rejected, with the reason for rejection as the first argument. The reason may be any valid JavaScript value.

will be called after the promise is rejected, with the reason for rejection as the first argument. The reason may be any valid JavaScript value. Neither onFulfilled() nor onRejected() may be called more than once.

be called more than once. .then() may be called many times on the same promise. In other words, a promise can be used to aggregate callbacks.

may be called many times on the same promise. In other words, a promise can be used to aggregate callbacks. .then() must return a new promise.

Promise Chaining

.then should return promise thats why we can create a chain of promises like this

Promise .then(() => Promise.then(() => Promise.then(result => result) )).catch(err)

Resolving a promise

Let’s see the resolve function definition that we left earlier before moving to .then()

function resolve(self, newValue) { // Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure if (newValue === self) { return reject( self, new TypeError("A promise cannot be resolved with itself.") ); } if ( newValue && (typeof newValue === "object" || typeof newValue === "function") ) { var then = getThen(newValue); if (then === IS_ERROR) { return reject(self, LAST_ERROR); } if (then === self.then && newValue instanceof Promise) { self._state = 3; self._value = newValue; finale(self); return; } else if (typeof then === "function") { doResolve(then.bind(newValue), self); return; } } self._state = 1; self._value = newValue; finale(self); }

Basically, we check if result it is a promise or not if it’s not a promise then if its a function then call that function with value using doResolve()

If result is a promise then it will be pushed to deferreds array, you can find this logic in finale function

Rejecting a promise:

Promise.prototype['catch'] = function (onRejected) { return this.then(null, onRejected); };

Above function can be found in ./es6-extensions.js

Whenever we reject a promise the .catch callback is called which is a sugar coat for then(null, onRejected)

Here is the basic rough diagram that I have created which is birds eye view of whats happening inside

Let’s see once again how everything is working:

For example we have this promise:

new Promise((resolve, reject) => { setTimeout(() => { resolve("Time is out"); }, 3000) }) .then(console.log.bind(null, 'Promise is fulfilled')) .catch(console.error.bind(null, 'Something bad happened: '))

Promise constructor is called and instance is created with new Promise executor function is passed to doResolve(executor, this) and callback where we have defined setTimeout will be called by tryCallTwo(executor, resolveCallback, rejectCallback) so it will take 3 seconds to finish We are calling .then() over promise instance so before our timeout is completed or any async api returns, Promise.prototype.then will be called as .then(cb, null) .then creates new promise and pass it as arguments to new Handler(onFulfilled, onRejected, promise) handle function is called with original promise instance and handler instance we created in point 4. Inside handle function current self._state = 0 and self._deferredState = 0 so self_deferredState will become 1 and handler instance will be assigned to self.deferreds after that control will return from there After .then() we are calling .catch() which will internally call .then(null, errorCallback) again the same steps are repeated from point 4 to point 6 and skip point 7 since we called .catch once Current promise state is pending and it will wait until is resolved or rejected so in this example after 3 seconds setTimeout callback is called and we are resolving explicitly which will call resolve(value) . resolveCallback will be called with value Time is out 🙂 and it will call main resolve function which will check if value !== null && value == 'object' && value === 'function' It will fail in our case since we passed string and self._state will become 1 with self._value = 'Time is out' and later finale(self) is called. finale will call handle(self, self.deferreds) once because self._deferredState = 1 in chain of promises it will call handle() for each deferred function . In handle function since promise is resolved already it will call handleResolved(self, deferred) handleResolved function will check if _state === 1 and assign cb = deferred.onFulfilled which is our then callback and later tryCallOne(cb, self._value) will call that callback and we get the final result while doing this if any error occurred then promise will be rejected.

When promise is rejected

In that case all the steps will remain same but in point 8 and we will call reject(reason) which will indirectly call rejectCallback defined in doResolve() and self._state will become 2 and in finale function cb will be equal to deferred.onRejected which will be called later by tryCallOne that’s how .catch will be called.

Thats all for now, I hope you enjoyed the article and it helps in your next JS interview.

if you encounter any problem feel free to get in touch or comment below.

I would be happy to help 🙂