A comprehensive guide on Promise polyfills
My intention here is to write a one-stop article to summarise how one would write all the polyfills for Promise and its static methods.
Topics coveredโ
new Promise(cb)Promise.resolvePromise.rejectPromise.withResolversPromise.racePromise.anyPromise.allSettled
new Promise(cb)โ
Here's what a typical promise syntax looks like:
const transaction = new Promise((resolve, reject) => {
try {
...
// do something async
...
if(success) {
resolve("๐ฐ๐ฐ๐ฐ")
}
} catch (err) {
console.error(err)
reject("Transaction failed")
}
if(somethingWrong){
reject("Transaction failed")
}
})
.then(console.log) // logs "๐ฐ๐ฐ๐ฐ" if succeeds.
.catch(console.error) // error-logs "Transaction failed" if fails.
from the syntax about, i hope it's pretty clear that this should be the skeleton of the polyfill class
class MyPromise {
constructor(callback){
// do something here
// pass the resolve/reject handlers to the callback
}
then(thenHandler){
// do something with the then callback
return this; // do not forget this
}
catch(catchHandler){
// do something with the catch callback
return this;
// Must if you want MyPromise to support `finally` cycle
}
}
let's zoom into what is needed in the constructor
class MyPromise {
constructor(callback){
// at some point we need to call the callback
// and provide it the resolve & reject handlers
function resolve(val) { // do something with val }
function reject(err) { // do something with err }
callback(resolve, reject);
}
}
New challenge, to complete the resolve and reject functions, we need to provide it with the thenHandler and the catchHandler. Because I've already used a class notation, I can hold these in some class fields.
class MyPromise {
onResolve;
onReject;
constructor(callback){
// same as before
}
then(thenHandler){
this.onResolve = thenHandler;
return this;
}
catch(catchHandler){
this.onReject = catchHandler;
return this;
}
}
There is a connection we are missing. When we call the onResolve or onReject callbacks, we need to provide these callbacks a value, however, the value would be available only in the constructor. So we need to add two more class fields to hold those values.
class MyPromise {
onResolve;
onReject;
value;
error;
constructor(callback){
function resolve(val) {
this.value = val;
}
function reject(err) {
this.error = err;
}
callback(resolve, reject)
}
// same as before
}
Guess what, we've opened another can of worms, when & where do we call our onResolve and onReject callbacks? Think opportunistically, and you'll see you got to do this both at the constructors and member functions. Because of this, we need to also maintain a state for promise being called, rejected or resolved. Now we have all the right ingredients.
class MyPromise {
onResolve;
onReject;
value;
error;
isFulfilled = false;
isRejected = false;
isCalled = false;
constructor(callback){
function resolve(val) {
this.isFulfilled = true;
this.value = val;
// Safety check to confirm that onResolve is set
if(typeof this.onResolve === 'function' && !this.isCalled) {
this.isCalled = true;
this.onResolve(val);
}
}
function reject(err) {
this.isRejected = true;
this.error = err;
if(typeof this.onReject === 'function' && !this.isCalled) {
this.isCalled = true;
this.onReject(err);
}
}
callback(resolve, reject);
}
then(thenHandler){
this.onResolve = thenHandler;
if(!this.isCalled && this.isFulfilled) {
this.isCalled = true;
this.onResolve(this.value); // OR thenHandler(this.value)
}
return this
}
catch(catchHandler){
this.onReject = catchHandler;
if(!this.isCalled && this.isRejected) {
this.isCalled = true;
this.onReject(this.error); // OR catchHandler(this.value)
}
return this;
}
}
So we are done? Let's try running this, and we'll uncover 2 major issues.

- All our internal class fields are exposed. Apart from
then&catchwe don't want anything else to be accessible. (Like in promises) - IT'S BROKEN!
I understand my priorities are not in the right order, but these are manageable issues. The extra exposed class fields can be obfuscated by using private fields.
The error is a very familiar one, if you've ever dealt with closures or context is javascript.
class MyPromise {
//same as before
constructor(callback){
function resolve(val) { // same as before }
function reject(err) { // same as before }
callback(resolve.bind(this), reject.bind(this))
}
// same as before
}
Why does this happen? the callbacks supplied to callback run in a different context. They loose the reference to this, hence cannot call or access any class fields they might depend upon. Another way to fix this would be to use arrow functions.
That's it. Your promise base class is now ready! ๐ฅณ
class MyPromise {
#onResolve;
#onReject;
#isFulfilled = false;
#isRejected = false;
#isCalled = false;
#value;
#error;
constructor(executor) {
function resolve(val){
this.#isFulfilled = true;
this.#value = val;
if(typeof this.#onResolve === 'function' && !this.#isCalled){
this.#isCalled = true;
this.#onResolve(val);
}
}
function reject(err){
this.#isRejected = true;
this.#error = err;
if(typeof this.#onReject === 'function' && !this.#isCalled){
this.#isCalled = true;
this.#onReject(err);
}
}
executor(resolve.bind(this), reject.bind(this));
}
then(thenHandler){
this.#onResolve = thenHandler;
if(!this.#isCalled && this.#isFulfilled){
this.#isCalled = true;
this.#onResolve(this.#value);
}
return this;
}
catch(catchHandler) {
this.#onReject = catchHandler;
if(!this.#isCalled && this.#isRejected){
this.#isCalled = true;
this.#onReject(this.#error);
}
return this;
}
}
Here are some things you could try to test yourself
- try to implement this yourself with
function(i.e. without usingclass) - accomodate
.finally. (We'll be needing this for one of the methods)
With this behind us, let's write the polyfills for the static methods. Promise.resolve and Promise.reject are the only ones that run against a value. Promise.all, Promise.race, Promise.allSettled Let's start with the easy ones
Promise.resolveโ
Promise.resolve resolves a value to a promise.
MyPromise.resolve = function(val) {
return new MyPromise(resolve => resolve(val));
}
Promise.rejectโ
Promise.reject rejects a value to a promise.
MyPromise.resolve = function(err) {
return new MyPromise((_, reject) => reject(err));
}
Promise.withResolversโ
This method, takes no parameters, and returns an object containing promise, resolve and reject.
MyPromise.withResolvers = function(){
let resolve, reject;
const promise = new MyPromise((res, rej) => {
resolve = res;
reject = rej;
});
return {resolve, reject, promise};
}
All promise methods that accept array of promises, accept empty array as input.
Promise.raceโ
This method accepts an array of promises, returns a single promise that resolves or rejects when any one of them fulfils or rejects.
MyPromise.race = function(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(task => {
MyPromise.resolve(task)
.then(resolve)
.catch(reject);
});
});
}
The order in which promises are resolve or rejected are persisted. Because there are no guarantees of promises arriving at a done state another, you cannot create an array and push values as you resolve (or reject) to them. They have to be added at the right indexes.
Promise.allโ
This method accepts and array of promises, returns a single promise that resolves when all promises fulfil, and fails when any one of them fails.
MyPromise.all = function(promises) {
return new MyPromise((resolve, reject) => {
if(!promises.length) {
resolve([]);
}
const resolved = [];
let resolveCount = 0;
promises.forEach((task, idx) => {
MyPromise.resolve(task)
.then(value => {
resolved[idx] = value;
resolveCount++;
if(resolveCount === iterable.length){
resolve(resolved);
}
})
.catch(reject);
});
});
}
Promise.anyโ
This method accepts an array of promises, returns a single promise that resolves with the first promise that fulfils, else rejects with an AggregateError.
MyPromise.any = function(promises) {
return new MyPromise((resolve, reject) => {
if(!promises.length) {
reject(new AggregateError([]));
}
const errors = [];
let errorCount = 0;
promises.forEach((task, idx) => {
MyPromise.resolve(task)
.then(resolve)
.catch(error => {
errors[idx] = error;
errorCount++;
if(errorCount === promises.length){
reject(new AggregateError(errors));
}
});
});
});
}
Promise.allSettledโ
This method accepts an array of promises, and returns a promise that (always) resolves with status of each promise and value (or reason) of each of them.
// don't run this without implementing `.finally` on MyPromise
MyPromise.allSettled = function(promises){
return new MyPromise(resolve => {
if(!promises.length) {
resolve([]);
}
let calledCount = 0;
const results = [];
promises.forEach((task, idx) => {
// Use native Promise if `.finally` isn't implemented
MyPromise.resolve(task)
.then(value => {
results[idx] = {status: "fulfilled", value};
})
.catch(reason => {
results[idx] = {status: "rejected", reason};
})
.finally(() => {
calledCount++;
if(calledCount === promises.length){
resolve(results);
}
});
});
});
}
Resourcesโ
That about it!