Polyfills for Javascript Promise combinators and Promise API

ยท

7 min read

In JavaScript, promises are objects representing the eventual completion or failure of an asynchronous operation. They allow handling of asynchronous operations in a more manageable way

  • we pass every promise a callback fn , with other two callbacks as function arguments

Promise API Polyfill

Forming Basic Structure

Before we start to create our own implementation of of JS promise api we need to make a few observations

  • Constructor: Promise takes in a callback and executes it immediately. The synchronous code runs right away

  • Private Functions: resolve and reject are private functions(#) defined within Promise encapsulation, executed via virtue of closures, lets call then onSuccess and onFail internally

  • Private Members: Promise maintains an internal state [pending,resolved,rejected]

  • Public Functions: then ,catch and finally are the only public member functions of the Promise encapsulation.

    • Both then and catch can be further chained, this implies they must themselves return promises
  • Private Members: There can be multiple then for any promise so be need to store these callbacks in an array, similar functionality is needed for catch

  • 'resolve' and `reject` can only run once for a promise so we need to place state checks before running these

  • The second argument of then serves as a catch, for that block. This implies catch can internally be implemented via calling then only


const STATE = {
    FULFILLED: "fulfilled",
    REJECTED: "rejected",
    PENDING: "pending",
}

class MyPromise {
    #thenCbs = []
    #catchCbs = []
    #state = STATE.PENDING
    #value // value from resolve or reject
    constructor(cb) {
        try {
            cb(this.#onSuccess, this.#onFail)// call function rightway with two functions as aguments
        } catch (err) {
            this.#onFail(err);
        }
    }

    #runThenOrCatchCallbacks() {
        if (this.#state === STATE.FULFILLED) {
            this.#thenCbs.forEach(callback => {
                callback(this.#value)// calling all the cbs via response from resolve reject
            })

            this.#thenCbs = []//resetting callbacks
        }

        if (this.#state === STATE.REJECTED) {
            this.#catchCbs.forEach(callback => {
                callback(this.#value)
            })

            this.#catchCbs = []
        }
    }

    #onSuccess(value) {// polyfill for resolve()
        if (this.#state !== STATE.PENDING) return
        this.#value = value
        this.#state = STATE.FULFILLED
        this.#runThenOrCatchCallbacks();
    }

    #onFail(value) {// polyfill for reject()
        if (this.#state !== STATE.PENDING) return
        this.#value = value
        this.#state = STATE.REJECTED
        this.#runThenOrCatchCallbacks();
    }

    then(thencb, catchcb) {
        if (thencb != null) this.#thenCbs.push(thencb);
        if(catchcb!=null) this.#catchCbs.push(catchcb);
        this.#runThenOrCatchCallbacks();// you may call then on a function even after promise has
        // already finished resolving - it will simply run thencbs or catchcbs
    }

    catch(catchcb) {
        this.then(undefined, catchcb);
    }

    finally(cb) {
        return this.then(
            result => {
                cb()
                return result
            },
            result => {
                cb()
                throw result
            }
        )
    }
}

module.exports = MyPromise

Implementing chaining functionality

  • Currently our onSuccess and onFail are not properly bound to `this` this will create an issue while chaining so we will use "bind" function to properly set the context

  • Currently our then is not returning a promise but in order to chain me must do that

  • Consider the case MyPromise.then(thencb1).catch(catchcb).then(thencb2) in this case if the promise resolves we want to run the second thencb2 with the result of thencb1(value) where value is returned from the promise . We modify our code to handle this chaining

  • We need to handle if the result of `resolve` is itself a promise as well

  • We also need to make run our resolve and reject ALWAYS run asynchronously we can do this by using a `queueMicrotask`

  • when we don't have any catch defined on promise while there is an error we throw an UnCaughtPromiseError

  • Now we can move on to implementing static methods resolve and reject

We can also incorporate the other static methods all/allSettled/race and any as discussed above

Final Code

const STATE = {
    FULFILLED: "fulfilled",
    REJECTED: "rejected",
    PENDING: "pending",
}

class MyPromise {
    #thenCbs = []
    #catchCbs = []
    #state = STATE.PENDING
    #value
    #onSuccessBind = this.#onSuccess.bind(this)
    #onFailBind = this.#onFail.bind(this)

    constructor(cb) {
        try {
            cb(this.#onSuccessBind, this.#onFailBind)
        } catch (e) {
            this.#onFail(e)
        }
    }

    #runCallbacks() {
        if (this.#state === STATE.FULFILLED) {
            this.#thenCbs.forEach(callback => {
                callback(this.#value)
            })

            this.#thenCbs = []
        }

        if (this.#state === STATE.REJECTED) {
            this.#catchCbs.forEach(callback => {
                callback(this.#value)
            })

            this.#catchCbs = []
        }
    }

    #onSuccess(value) {
        queueMicrotask(() => {
            if (this.#state !== STATE.PENDING) return

            if (value instanceof MyPromise) {
                value.then(this.#onSuccessBind, this.#onFailBind)
                return
            }

            this.#value = value
            this.#state = STATE.FULFILLED
            this.#runCallbacks()
        })
    }

    #onFail(value) {
        queueMicrotask(() => {
            if (this.#state !== STATE.PENDING) return

            if (value instanceof MyPromise) {
                value.then(this.#onSuccessBind, this.#onFailBind)
                return
            }

            if (this.#catchCbs.length === 0) {
                throw new UncaughtPromiseError(value)
            }

            this.#value = value
            this.#state = STATE.REJECTED
            this.#runCallbacks()
        })
    }

    then(thenCb, catchCb) {
        return new MyPromise((resolve, reject) => {
            this.#thenCbs.push(result => {
                if (thenCb == null) {
                    resolve(result);
                    return
                }

                try {
                    resolve(thenCb(result))
                } catch (error) {
                    reject(error)
                }
            })

            this.#catchCbs.push(result => {
                if (catchCb == null) {
                    reject(result)
                    return
                }

                try {
                    resolve(catchCb(result))
                } catch (error) {
                    reject(error)
                }
            })

            this.#runCallbacks()
        })
    }

    catch(cb) {
        return this.then(undefined, cb)
    }

    finally(cb) {
        // never gets any value passed to it 
        return this.then(
            result => {
                cb()
                return result
            },
            result => {
                cb()
                throw result
            }
        )
    }

    static resolve(value) {
        return new MyPromise(resolve => {
            resolve(value)
        })
    }

    static reject(value) {
        return new MyPromise((resolve, reject) => {
            reject(value)
        })
    }

    static all(promises) {
        const results = []
        let completedPromises = 0
        return new MyPromise((resolve, reject) => {// new promise for all promises
            for (let i = 0; i < promises.length; i++) {
                const promise = promises[i]
                promise
                    .then(value => {
                        completedPromises++
                        results[i] = value
                        if (completedPromises === promises.length) {
                            resolve(results)
                        }
                    })
                    .catch(reject)
            }
        })
    }

    static allSettled(promises) {
        const results = []
        let completedPromises = 0
        return new MyPromise(resolve => {
            for (let i = 0; i < promises.length; i++) {
                const promise = promises[i]
                promise
                    .then(value => {
                        results[i] = { status: STATE.FULFILLED, value }
                    })
                    .catch(reason => {
                        results[i] = { status: STATE.REJECTED, reason }
                    })
                    .finally(() => {
                        completedPromises++;// always increments regardless of success or fail
                        if (completedPromises === promises.length) {
                            resolve(results)
                        }
                    })
            }
        })
    }

    static race(promises) {
        return new MyPromise((resolve, reject) => {
            promises.forEach(promise => {
                promise.then(resolve).catch(reject)
            })
        })
    }

    static any(promises) {
        const errors = []
        let rejectedPromises = 0
        return new MyPromise((resolve, reject) => {
            for (let i = 0; i < promises.length; i++) {
                const promise = promises[i]
                promise.then(resolve).catch(value => {
                    rejectedPromises++
                    errors[i] = value
                    if (rejectedPromises === promises.length) {
                        reject(new AggregateError(errors, "All promises were rejected"))
                    }
                })
            }
        })
    }
}

class UncaughtPromiseError extends Error {
    constructor(error) {
        super(error)
        this.stack = `(in promise) ${error.stack}`
    }
}

module.exports = MyPromise

Promise Combinators Polyfills

Promise combinators are higher-order functions used to work with multiple promises simultaneously. Common promise combinators include Promise.all, Promise.allSettled,Promise.any and Promise.race

Promise.all polyfill

The Promise.all() static method takes an iterable of promises as input and returns a single Promise. This returned promise fulfills when all of the input's promises fulfill (including when an empty iterable is passed), with an array of the fulfillment values. It rejects when any of the input's promises rejects, with this first rejection reason.

below is a quick polyfill for Promise.all

Promise.allSettled

The Promise.allSettled() static method takes an iterable of promises as input and returns a single Promise. This returned promise fulfills when all of the input's promises settle (resolve/reject), with an array of objects that describe the outcome of each promise. This promise never rejects

below is a quick polyfill for Promise.allSettled

Promise.any

The Promise.any() static method takes an iterable of promises as input and returns a single Promise. This returned promise fulfills when any of the input's promises fulfills, with this first fulfillment value. It rejects only when all of the input's promises reject (including when an empty iterable is passed), with an AggregateError containing an array of rejection reasons.

below is a quick polyfill for Promise.any

Promise.race

The Promise.race() static method takes an iterable of promises as input and returns a single Promise. This returned promise settles with the eventual state of the first promise that settles(resolves/rejects)

below is a quick polyfill for Promise.race

Promise Combinator polyfills summary

Promise combinator(all take in array of promises)Working
all - all promises should resolveshort-circuits with reject when an input promise is rejected otherwise returns an array of results iff ALL promises resolve
allSettled - all promises should settledoes not short-circuit, returns the resolve/reject values/error messages for EACH promise with a status appended to the response**, this promise never rejects**
any - any promise should resolveshort circuits with the first resolve,but returns an array of error messages iff ALL promises reject
race - any promise should settleshort circuits with the first resolve or reject
ย