Intro What is a promise, why are they useful? What are callbacks ? Drawbacks of callbacks and the solutionWhat are the different states of a promise ?How to create and consume a promise ?Promise methods and their polyfills Promise.allPromise.all polyfillPromise.racePromise.race polyfillPromise.anyPromise.any polyfillPromise.allSettled Promise.allSettled polyfillAsync / AwaitError handling conclusion
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 function
The 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
resolve
with 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
- It will return a promise.
- The promise will resolve with result of all the passed promises or reject with the error message of first failed promise.
- 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 - It returns a promise.
- The returned promise fulfils or rejects as soon as any one of the input promises fulfils or rejects.
- 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- Function takes an array of promises as input and returns a new promise.
- The returned promise is resolved as soon as any of the input promises resolves.
- 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 asPromise.reject("Noo")
Promise.resolve
: Resolves a Promise,new Promise(res => res("yaay"))
is same asPromise.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 byPromise.all
immediately rejects with that error, in case of an error other promises are ignored.
Promise.race
:Promise.race
is similar toPromise.all
, but only waits for the first settled promise and gets its result (or error).
Promise.any
:Promise.any
is similar toPromise.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 withAggregateError
– a special error object that stores all promise errors initserrors
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.