Promises and its polyfills.

Promises and its polyfills.

Published
November 15, 2022
 
 

Intro

 
In this blog we will understand what Promises are in JavaScript, why they are useful. We will cover Promise methods and write polyfills for them to understand how they work under the hood. We will also understand what async/await are and how they work under the hood.

What is a promise, why are they useful?

 
Promises are an alternative to callbacks for delivering the results of an asynchronous computation. By the MDN Definition , The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.
 
First let’s understand what callbacks are

What are callbacks ?

 
A callback function is a function passed into another function as an argument, which is then invoked inside that function to complete some kind of action. Callbacks are usually used when we want to wait for the result of a previous function call and then execute another function call.
 
Here is an example.
 
function greeting(name) { alert('Hello ' + name); } function processUserInput(callback) { var name = prompt('Please enter your name.'); callback(name); } processUserInput(greeting);
 
The above example is a synchronous callback, as it is executed immediately.
 
Note, however, that callbacks are often used to continue code execution after an  asynchronous operation has completed — these are called asynchronous callbacks. An example of this would be the callback function the addEventListener function takes as the second parameter.
 
document.getElementById("btn").addEventListener('click', () => { alert('you have clicked the button') });
 
Let’s take a look at another example that uses the setTimeout() method to mimic the program that takes time to execute, such as data coming from the server
 
// program that shows the delay in execution function greet() { console.log('Hello world'); } function sayName(name) { console.log('Hello' + ' ' + name); } // calling the function setTimeout(greet, 2000); sayName('John');
 
output would be
 
Hello John Hello world
 
As we know, the setTimeout() method executes a block of code after the specified time.
 
Here, the greet() function is called after 2000 milliseconds (2 seconds). During this wait, the sayName('John'); is executed. That is why Hello John is printed before Hello world.
 
The above code is executed asynchronously (the second function; sayName() does not wait for the first function; greet() to complete).
 

Drawbacks of callbacks and the solution

 
Let’s take a take a look at this code which has callbacks inside callbacks. We have a loadScript function to load sequentially: the first one, and then the second one after it and so on. The natural solution would be to put the second loadScript call inside the callback, After the outer  loadScript is complete, the callback initiates the inner one.
loadScript('/my/script.js', function(script) { loadScript('/my/script2.js', function(script) { loadScript('/my/script3.js', function(script) { // ...continue after all scripts are loaded }); }); });
 
The problem here is we need to keep passing callbacks if we want to load more scripts one after another. This makes it had to read and theres a term for this, callback hell. So to avoid this we have Promises.

What are the different states of a promise ?

 
A Promise is always in either one of three (mutually exclusive) states:
 
  • Pending: the result hasn’t been computed yet. (the initial state of each Promise).
  • Fulfilled: the result was computed successfully.
  • Rejected: a failure occurred during computation.

How to create and consume a promise ?

 
You can create promise using the constructor method
 
let promise = new Promise(function(resolve, reject) { // Do something and either resolve or reject });
 
We need to pass a function to the Promise Constructor. That function is called the executor functionThe executor function takes two arguments, resolve and reject. These two are callback functions for the executor to announce an outcome of the promise. The resolve method indicates successful completion of the task, and the reject method indicates an error.
We can consume a promise by using .then() .catch() and .finally(). All of these methods return a promise and therefore we can chain them as well.
 
promise .then(value => { /* fulfillment */ }) .catch(error => { /* rejection */ }); .finlly(value => { /* always runs this */ })
 
Let’s take a look at this fakeFetch function.
 
const fakeFetch = (msg, shouldReject) => { return new Promise((resolve, reject) => { setTimeout(() => { if (shouldReject) { reject(console.log("Rejected")) } resolve(console.log(`from server: ${msg}`)) }, 3000); }) } fakeFetch("hi", true)
 
Lets take a look at another example of setTimeout() as the Promise-based function delay()
function delay(ms) { return new Promise(function (resolve, reject) { setTimeout(resolve, ms); }); }
 
we are calling resolvewith zero parameters, which is the same as calling resolve(undefined). We don’t need the fulfilment value when we are using delay(), so we simply ignore it. Just being notified is enough here.
 
Using delay():
 
delay(5000).then(function () { console.log('5 seconds have passed!') });
 
 
FYI, when you know that a promise will always resolve or always reject, you can write Promise.resolve or Promise.reject , with the value you want to reject or resolve the promise with!
 
new Promise(res => res("yaay"))
 
is the same as
 
Promise.resolve("yaay")
 
and
 
new Promise((res, rej) => rej("Nooo"))
 
is the same as
 
Promise.reject("yaay")
 
 

Promise methods and their polyfills

 
There are 6 static methods in the Promise class. We’ll quickly cover their use cases here.
 
We have already looked at Promise.resolve and Promise.reject. The other four are
 
  • Promise.all
  • Promise.race
  • Promise.any
  • Promise.allSettled
 

Promise.all

 
Let’s say we want many promises to execute in parallel and wait until all of them are ready.
For instance, download several URLs in parallel and process the content once they are all done.
 
That’s what Promise.all is for.
 
The syntax is:
 
let promise = Promise.all(iterable);
 
Promise.all takes an itereable (usually, an array of promises) and returns a new promise.
 
The new promise resolves when all listed promises are resolved, and the array of their results becomes its result.
 
For instance, the Promise.all below settles after 3 seconds, and then its result is an array [1, 2, 3]:
 
Promise.all([ new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1 new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2 new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3 ]).then(alert); // 1,2,3 when promises are ready: each promise contributes an array member
 
Please note that the order of the resulting array members is the same as in its source promises. Even though the first promise takes the longest time to resolve, it’s still first in the array of results.
 
if any of the promises is rejected, the promise returned by Promise.all immediately rejects with that error.
 
For instance:
 
Promise.all([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).catch(alert); // Error: Whoops!
 
In case of an error, other promises are ignored
 
If one promise rejects, Promise.all immediately rejects, completely forgetting about the other ones in the list. Their results are ignored like in the example above, and one fails, the others will still continue to execute, but Promise.all won’t watch them anymore. They will probably settle, but their results will be ignored.
 

Promise.all polyfill

 
Let’s write a polyfill for Promise.all to understand how it works under the hood, this is also sometimes asked in interviews. By the definition above we know that
 
  1. It will return a promise.
  1. The promise will resolve with result of all the passed promises or reject with the error message of first failed promise.
  1. The results are returned in the same order as the promises are in the given array.
 
const myPromiseAll = (promises) => { let results = []; let promisesResolved = 0; return new Promise((res, rej) => { promises.forEach((promise, index) => { //if promise passes, store its outcome and increment the promisesResolved count promise .then((val) => { results[index] = val; promisesResolved += 1; //if all the promises are completed resolve and return the result if (promisesResolved === promises.length) { res(results); } }) //if any promise fails, reject. .catch((error) => { rej(error); }); }); }); };
 
 

Promise.race

 
Promise.race is similar to Promise.all, but only waits for the first settled promise and gets its result (or error).
 
The syntax is:
 
let promise = Promise.race(iterable);
 
For instance, here the result will be 1:
 
Promise.race([ new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).then(alert);
 
The first promise here was fastest, so it became the result. After the first settled promise “wins the race”, all further results/errors are ignored.
 

Promise.race polyfill

 
Let’s write a polyfill for Promise.race to understand how it works under the hood, this is also sometime asked in interviews. By the definition above we know that
 
  1. It returns a promise.
  1. The returned promise fulfils or rejects as soon as any one of the input promises fulfils or rejects.
  1. Returned promise resolves with the value of the input promise or rejects with the reason of the input promise.
 
const myPromiseRace = (promises) => { return new Promise((resolve, reject) => { promises.forEach((promise) => { Promise.resolve(promise) .then(resolve) // resolve outer promise, as and when any of the input promise resolves .catch(reject); // reject outer promise, as and when any of the input promise rejects }); }); };
 
 

Promise.any

 
Promise.any is similar to Promise.race, but only waits for the first fulfilled promise and gets its result. If all of the given promises are rejected, then the returned promise is rejected with  AggregateError – a special error object that stores all promise errors inits errors property.
 
The syntax is:
 
let promise = Promise.any(iterable);
 
 
For instance, here the result will be 1:
Promise.any([ new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 1000)), new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)), new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000)) ]).then(alert); // 1
 
The first promise here was fastest, but it was rejected, so the second promise became the result. After the first fulfilled promise “wins the race”, all further results are ignored.
 
Here’s an example when all promises fail:
Promise.any([ new Promise((resolve, reject) => setTimeout(() => reject(new Error("Ouch!")), 1000)), new Promise((resolve, reject) => setTimeout(() => reject(new Error("Error!")), 2000)) ]).catch(error => { console.log(error.constructor.name); // AggregateError console.log(error.errors[0]); // Error: Ouch! console.log(error.errors[1]); // Error: Error! });
 
As you can see, error objects for failed promises are available in the errors property of the AggregateError object.
 

Promise.any polyfill

 
Let’s write a polyfill for Promise.any to understand how it works under the hood, this is also sometime asked in interviews. By the definition above we know that
 
  1. Function takes an array of promises as input and returns a new promise.
  1. The returned promise is resolved as soon as any of the input promises resolves.
  1. If all of the input promises are rejected then the returned promise is rejected with the array of all the input promises reasons.
 
const myPromiseAny = (promises) => { const promiseErrors = new Array(promises.length); let counter = 0; //return a new promise return new Promise((resolve, reject) => { promises.forEach((promise) => { Promise.resolve(promise) .then(resolve) // resolve, when any of the input promise resolves .catch((error) => { promiseErrors[counter] = error; counter = counter + 1; // if all promises are rejected, reject outer promise if (counter === promises.length) { reject(promiseErrors); } }); // reject, when any of the input promise rejects }); }); };
 

Promise.allSettled

 
Promise.allSettled just waits for all promises to settle, regardless of the result. The resulting array has:
 
  • {status:"fulfilled", value:result} for successful responses,
  • {status:"rejected", reason:error} for errors.
For example, we’d like to fetch the information about multiple users. Even if one request fails, we’re still interested in the others.
 
Let’s use Promise.allSettled:
 
let urls = [ 'https://api.github.com/users/vedanthb', 'https://api.github.com/users/batman', 'https://no-such-url' ]; Promise.allSettled(urls.map(url => fetch(url))) .then(results => { // (*) results.forEach((result, num) => { if (result.status == "fulfilled") { alert(`${urls[num]}: ${result.value.status}`); } if (result.status == "rejected") { alert(`${urls[num]}: ${result.reason}`); } }); });
 
The results in the line (*) above will be:
 
[ {status: 'fulfilled', value: ...response...}, {status: 'fulfilled', value: ...response...}, {status: 'rejected', reason: ...error object...} ]
 
So for each promise we get its status and value/error.
 

Promise.allSettled polyfill

 
Let’s write a polyfill for Promise.allSettled to understand how it works under the hood, this is also sometime asked in interviews. By the definition above we know that
 
  • Map the array of promises to return an object with status and value/error depending upon the promised settlement.
  • Pass this map to the Promise.all to run them at once and return the result.
 
const myPromiseAllSettled = (promises) => { const rejectHandler = (reason) => ({ status: "rejected", reason }); const resolveHandler = (value) => ({ status: "fulfilled", value }); const convertedPromises = promises.map((promise) => Promise.resolve(promise).then(resolveHandler, rejectHandler) ); return Promise.all(convertedPromises); };
 

Async / Await

 
Async/Await enables us to write asynchronous code in a synchronous fashion, which produces cleaner and easier-to-understand logic. Under the hood, it’s just syntactic sugar using generators and yield statements to “pause” execution. It allows us to work with promises with less boilerplate.
 
For example, the following definitions are equivalent:
 
function f() { return Promise.resolve('TEST'); } // asyncF is equivalent to f! async function asyncF() { return 'TEST'; }
 
Any async function returns a promise implicitly, and the resolve value of the promise will be whatever you return from the function
 
Async - declares an asynchronous function (async function someName(){...}).
 
  • Automatically transforms a regular function into a Promise.
  • When called async functions resolve with whatever is returned in their body.
  • Async functions enable the use of await.
 
Await - pauses the execution of async functions. (var result = await someAsyncCall()).
 
  • When placed in front of a Promise call, await forces the rest of the code to wait until that Promise finishes and returns a result.
  • This will pause the async function and wait for the Promise to resolve prior to moving on.
  • Await works only with Promises, it does not work with callbacks.
  • Await can only be used inside async functions.
 
Thumb Rules to remember while using async await
 
  • await blocks the code execution within the async function, of which it (i.e. the 'await' statement) is a part.
  • There can be multiple await statements within a single async function.
  • When using async await make sure to use try catch for error handling.
  • If my code contains blocking code it is better to make it an async function. By doing this I am making sure that somebody else can use your function asynchronously.
  • By making async functions out of blocking code, you are enabling the user who will call your function to decide on the level of asynchronicity he/she wants.
  • await only blocks the code execution within the async function. It only makes sure that next line is executed when the promise resolves. So if an asynchronous activity has already started then await will not have an effect on it.
 
Lets write a function syncCallsToServer(msg1, msg2) which will take two messages and call fakeFetch() with the second message only when the first message has returned from the server.
 
const fakeFetch = (msg, shouldReject) => { return new Promise((resolve, reject) => { setTimeout(() => { if (shouldReject) { reject("rejected"); } resolve(`from server ${msg}`); }, 3000); }); }; const syncCallsToServer = async (msg1, msg2) => { try { let dataFromMsg1 = await fakeFetch(msg1); let dataFromMsg2 = await fakeFetch(msg2); console.log({ msg1: dataFromMsg1, msg2: dataFromMsg2 }); } catch (error) { console.log(error); } }; syncCallsToServer("Hello", "Hi");
 
Believe it or not, Async/Await is just syntactic sugar. We will examine how it all works under the hood. To do that, we will attempt to recreate the async function from scratch!
 
The key insight is to use Generators. Ultimately, we want to create an async function that takes in a generator. That way, we can nest yield statements that look like this:
 
async(function* () { const response = yield fetch('https://api.openweathermap.org/data/2.5/weather?zip=90813&APPID=5dd3e57156e6ebba767abb71509b53a0'); const data = yield response.json(); console.log(data); });
 
In fact, await statements are just yield statements under the hood!
 
Thus, we need our async “polyfill” to instantiate the generator, recursively nesting next for each yield statement inside its Promise’s then callback until done is true. In other words, when each Promise resolves, its value will be passed as an argument to next, whereby g.next will replace its corresponding yield statement with that value.
 
async = generator => { const g = generator(); (function next(value) { const n = g.next(value); if (n.done) return; n.value.then(next); }()); }
 
In effect, we can assign the value of any Promise to any variable inside the generator!
 
Clever, right? 😉
 
Async/Await enables us to write asynchronous code in a synchronous fashion, which produces cleaner and easier-to-understand logic. Under the hood, it’s just syntactic sugar using generators and yield statements to “pause” execution. In other words, async functions can “pull out” the value of a Promise even though it’s nested inside a callback function, giving us the ability to assign it to a variable!
 
In fact, generators and even Promises themselves are also “syntactic sugars”! — clever designs to help us avoid writing callback hell.
 

Error handling

 
If a promise resolves normally, then await promise returns the result. But in the case of a rejection, it throws the error, just as if there were a throw statement at that line.
 
This code:
 
async function f() { await Promise.reject(new Error("Whoops!")); }
 
…is the same as this:
 
async function f() { throw new Error("Whoops!"); }
 
In real situations, the promise may take some time before it rejects. In that case there will be a delay before await throws an error.
 
We can catch that error using try..catch, the same way as a regular throw:
 
async function f() { try { let response = await fetch('http://no-such-url'); } catch(err) { alert(err); // TypeError: failed to fetch } } f();

conclusion

 
  • The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value. They are alternatives to callbacks.
  • A callback function is a function passed into another function as an argument, which is then invoked inside that function to complete some kind of action. Callbacks are usually used when we want to wait for the result of a previous function call and then execute another function call.
  • A Promise is always in either one of three (mutually exclusive) states:
    • Pending: the result hasn’t been computed yet. (the initial state of each Promise).
    • Fulfilled: the result was computed successfully.
    • Rejected: a failure occurred during computation.
  • We can create a Promise using the constructor method , and we can consume a promise by using .then() .catch() and .finally(). All of these methods return a promise and therefore we can chain them as well.
    • let promise = new Promise(function(resolve, reject) { // Do something and either resolve or reject }); promise .then(value => { /* fulfillment */ }) .catch(error => { /* rejection */ }); .finlly(value => { /* always runs this */ })
 
  • There are 6 static methods in the Promise class.
  • Promise.reject : Rejects a Promise, new Promise((res, rej) => rej("Noo")) is same as Promise.reject("Noo")
  • Promise.resolve : Resolves a Promise, new Promise(res => res("yaay")) is same as Promise.resolve("yaay")
  • Promise.all : Promise.all takes an iterable (usually, an array of promises) and returns a new promise. The new promise resolves when all listed promises are resolved, and the array of their results becomes its result. if any of the promises is rejected, the promise returned by Promise.all immediately rejects with that error, in case of an error other promises are ignored.
  • Promise.race : Promise.race is similar to Promise.all, but only waits for the first settled promise and gets its result (or error).
  • Promise.any : Promise.any is similar to Promise.race, but only waits for the first fulfilled promise and gets its result. If all of the given promises are rejected, then the returned promise is rejected with  AggregateError – a special error object that stores all promise errors inits errors property.
  • Promise.allSettled : Promise.allSettled just waits for all promises to settle, regardless of the result. The resulting array has:
    • {status:"fulfilled", value:result} for successful responses,
    • {status:"rejected", reason:error} for errors.
 
  • Async/ Await: Async/Await enables us to write asynchronous code in a synchronous fashion, which produces cleaner and easier-to-understand logic. Under the hood, it’s just syntactic sugar using generators and yield statements to “pause” execution. It allows us to work with promises with less boilerplate. Any async function returns a promise implicitly, and the resolve value of the promise will be whatever you return from the function. await blocks the code execution within the async function, of which it (i.e. the 'await' statement) is a part.