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
andreject
are private functions(#
) defined within Promise encapsulation, executed via virtue of closures, lets call then onSuccess and onFail internallyPrivate Members: Promise maintains an internal state [pending,resolved,rejected]
Public Functions:
then
,catch
andfinally
are the only public member functions of the Promise encapsulation.- Both
then
andcatch
can be further chained, this implies they must themselves return promises
- Both
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 callingthen
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
andonFail
are not properly bound to `this` this will create an issue while chaining so we will use "bind" function to properly set the contextCurrently 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 chainingWe 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 resolve | short-circuits with reject when an input promise is rejected otherwise returns an array of results iff ALL promises resolve |
allSettled - all promises should settle | does 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 resolve | short circuits with the first resolve,but returns an array of error messages iff ALL promises reject |
race - any promise should settle | short circuits with the first resolve or reject |