Hey All π ,
This is my third article on JavaScript ES6 Concepts. If you have not read the other one yet, you can read it here - Classes, Modules. In this article I'm going to talk about all you need to get started with JavaScript Promises.
Table Of Contents -
- Introduction
- Callback functions
- Promises
- Promise Chaining
- Static methods
Introduction
While learning Javascript, you may have heard some fancy words like asynchronous, callbacks, promises, etc., which confuses many people.
So today, we'll try to remove this confusion as much as possible by talking about these topics.
Let's talk about what a promise is?
Promises are simply like real-world promises, for example, I promise to write an awesome article for you on mental health. So there can be two outcomes: either I'll fulfill(resolve) it or not(reject).
The same is the case with Javascript Promises. A promise is a part of code that promises to produce an output, so either it'll resolve it or reject it.
And according to the output, we can have code that handles the resolve or the reject.
Before Javascript Promises came into the picture, we were using callback functions for handling asynchronous code.
Let's talk about what asynchronous code means?
Javascript code are read line-by-line, and Asynchronous code is the code that takes some time to complete. So, they go outside of the main program flow, allowing the code after the asynchronous code to be executed immediately without waiting.
Let's understand this with an example -
// π main.js
console.log("Start");
console.log("Normal Flow");
console.log("End");
Let's see the output -
Here we can see that the code is read line-by-line and the output is produced accordingly.
Now let's see the same example with an asynchronous code -
// π main.js
console.log("Start");
// Asynchronous Code
setTimeout(() => {
console.log("Async code: It'll take some time...");
}, 3000);
console.log("End");
Here we can see we have added an asynchronous code which will take some time to complete. Let's see the output in this case -
We can see that in this case, when the asynchronous code was read, it came out of the normal flow as it took some time to complete, while during this, the next codes started executing without waiting for the asynchronous code to complete. And the output of the asynchronous code came when it completed its execution.
This was a small example. Some real-life examples are fetching data from a database or server, sending an image etc. These all take time to complete and can also fail and produce an error; thus, we need some ways to handle asynchronous code.
And so callback functions and Promises come into the picture.
Let's start with some basics of callback functions -
Callback functions
When a function is passed as an argument to another function, then it is called a callback function.
Let's understand how callback functions are used to handle the asynchronous code with an example -
// π main.js
console.log("Start");
const displayMiddle = () => {
console.log("middle: Iβm called by setTimeout so Iβll take some time to complete...");
};
const displayEnd = () => {
console.log("End");
};
// Asynchronous Code
setTimeout(displayMiddle, 3000);
displayEnd();
I've modified the first example slightly, but it is still the same as the functions are called sequentially. Also, it produces the same result as earlier -
Now let's see how we can use a callback function to handle this asynchronous code -
// π main.js
console.log("Start");
const displayMiddle = (callback) => {
console.log(
"middle: Iβm called by setTimeout so Iβll take some time to complete..."
);
// callback function will run only when outer function will complete
callback();
};
const displayEnd = () => {
console.log("End");
};
// Asynchronous Code; displayEnd() passed as an argument
setTimeout(displayMiddle, 3000, displayEnd);
Here we can see we have passed the displayEnd function as an argument to the displayMiddle function; thus, it is called a callback function.
Note: Notice we don't use parenthesis () while passing functions.
After passing the displayEnd function as a callback function, we place it at the last of the displayMiddle function. And now, when the displayMiddle function is called, it'll complete its execution, then only the displayEnd function will execute.
Let's see the output -
Here we can see the displayEnd function waits for the displayMiddle function to complete and then executes.
Problems with callback Functions -
It's not easy to handle complex asynchronous code with callbacks; it makes the code hard to read, hard to debug, and also, it's easier to break.
Another problem is something called Callback Hell. When we start nesting callbacks repeatedly, it results in a messier code that is very likely to break.
Let's see a small example -
// π main.js
function load(data, callback) {
console.log(data);
callback("right");
}
load("Alok", function (sign) {
if (sign === "right") {
load("Aman", function (sign) {
if (sign === "right") {
load("Rajan", function (sign) {
console.log("Done");
});
}
});
}
});
Output -
Here we can see that we have a function that keeps calling a callback function repeatedly. Even this code is hard to explain; now imagine replacing console.logs with conditions, loops, and so on. This results in a code that is easy to break and hard to manage.
We can handle such cases with Javascript Promises, but first, let's see what Javascript Promises are.
Promises
A promise is a special JavaScript object that links the βproducing codeβ and the βconsuming codeβ together.
Producing code: The code which takes some time to run.
Consuming code: The code which must wait for the result from producing code.
A Promise has three states -
1) pending - if the code is executing
2) fulfilled - if the code is executed successfully, then it produces a result
3) rejected - if any error occurs, then it produces an error
Let's understand Promises with the help of its syntax -
// π main.js
let promise = new Promise((resolve, reject) => {
// Some code which takes time to execute...
// if code executes successfully
resolve(result);
// if some error occurs
reject(error);
});
Using new Promise(), we can create a promise. It takes a function with two arguments - resolve and reject.
Both resolve and reject are callback functions that have specific purposes -
resolve - if the code runs successfully, resolve is called with the result.
reject - if any error occurs, reject is called with the error.
To summarize -
Now let's see an example -
// π main.js
let promise = new Promise((resolve, reject) => {
let x = 3;
if (x === 3) {
resolve("true");
} else {
reject("false");
}
});
Here we can see that we have created a promise which calls resolve or reject based on a condition.
Note: A promise can call only one callback function, either resolve or reject and if we add more resolve or reject, it'll consider the first and ignore the rest.
This was a producer code that created a promise. Now let's see how to use it with the help of consumers.
Consumers: then, catch, finally
then :
then is the most important and most used consumer. It allows us to handle the promise.
Let's see an example of how we can use the above promise using then -
// π main.js
let promise = new Promise((resolve, reject) => {
let x = 3;
if (x === 3) {
resolve("true");
} else {
reject("false");
}
});
// resolve runs the first function in .then
// reject runs the second function in .then
promise.then(
(resolver) => console.log(resolver), // true
(error) => console.log(error) // doesn't run
)
The first argument of then is a function that runs if the promise is resolved, while the second function runs if the promise is rejected.
Thus using these functions, we can handle the promise and use it as per our need, for example - loading a script(which takes some time to load) in the promise and then handle it using then - display the page( after the script is successfully loaded ).
catch :
We can also use catch to handle the promise if it is rejected, i.e., any error is produced ( just like try{...} catch{...} ).
Let's see an example -
// π main.js
let promise = new Promise((resolve, reject) => {
let x = 4;
if (x === 3) {
resolve("true");
} else {
reject("false");
}
});
// reject runs the code in catch
promise
.then((resolver) =>
console.log(resolver)) // doesn't run
.catch(
(error) => console.log(error) // false
);
Here we can see we have used the catch to handle the reject.
finally :
finally can be used to handle the promise when it is settled( either resolved or rejected, it doesn't matter ).
It is used when we have to do something irrespective of: the promise is resolved or rejected.
Letβs see an example -
// π main.js
let promise = new Promise((resolve, reject) => {
let x = 4;
if (x === 3) {
resolve("true");
} else {
reject("false");
}
});
promise
.then((resolver) =>
console.log(resolver)) // doesn't run
.catch(
(error) => console.log(error) // false
)
.finally(() =>
console.log("Computation Done!!!")); // Computation Done!!!
Promise Chaining
Remember we discussed something called Callback hell earlier in this article in which we have to perform a sequence of asynchronous tasks.
So let's see how we can handle that with the help of promises :
We use Promise chaining to achieve that. Let's see an example to understand it -
// π main.js
let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 5000);
});
// Promise chaining
promise
.then((resolver) => {
console.log(resolver);
return resolver + 1;
})
.then((resolver) => {
console.log(resolver);
return resolver + 1;
})
.then((resolver) => {
console.log(resolver);
})
.catch(() => console.log("Error Occurred"))
.finally(() => console.log("Done"));
Here we can see that we have used a chain of .then to perform a sequence of asynchronous tasks, after the chain of .then we have a catch block to handle the error if any produced, and at the very end, we have a finally block to do something when all the promises are settled.
When a .then return something, then it is passed to the next .then and so on until the promise is settled.
Note : Here a call to promise.then also returns a promise so we can call the next .then on it.
Let's have a look at the output -
Here we can see that all .thens ran one-by-one producing their result, i.e., 1, 2 and 3 and passing some value to the next .then and at last, the finally block ran producing Done.
And we can clearly see that it is much easier to read and understand and also easier to manage.
Static Methods
Let's talk about some of the static methods of the Promises which are very useful -
Promise.all
It takes an array of promises, runs all of them parallelly, and returns an array of results when all complete execution.
Let's see an example -
// π main.js
let promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("I'm Promise 1"), 3000);
});
let promise2 = new Promise((resolve, reject) => {
setTimeout(() => resolve("I'm Promise 2"), 2000);
});
let promise3 = new Promise((resolve, reject) => {
setTimeout(() => resolve("I'm Promise 3"), 1000);
});
// Passing an array of Promises
Promise.all([promise1, promise2, promise3]).then(
(resolvers) => console.log(resolvers) // (3) ["I'm Promise 1", "I'm Promise 2", "I'm Promise 3"]
);
Here we can see we have three promises. We passed them all as an array to Promise.all and handled them in .then which produces an array as a result.
The results array contains the output in order as the promises were passed, irrespective of which finishes first.
If there is an error in any of the promises, it throws an error. It only proceeds when all promises are successful.
Promise.allSettled
In Promise.all we proceed only when all Promises are successful while Promise.allSettled just waits till all the Promises are settled, irrespective of resolved or rejected.
It gives an array of objects as a result:
{status:"fulfilled", value:result} // if resolved
{status:"rejected", reason:error} // if rejected
Let's see an example -
// π main.js
let promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("I'm Promise 1"), 3000);
});
let promise2 = new Promise((resolve, reject) => {
setTimeout(() => reject("Ooops!!!"), 2000);
});
let promise3 = new Promise((resolve, reject) => {
setTimeout(() => resolve("I'm Promise 3"), 1000);
});
Promise.allSettled([promise1,promise2,promise3]).then((resolvers) =>
console.log(resolvers)
);
Letβs see the output -
Promise.race
Promise.race takes an array of promises and waits only for the first settled promise irrespective of resolved or rejected and gives the result or error.
Letβs see an example -
// π main.js
let promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("I'm Promise 1"), 3000);
});
let promise2 = new Promise((resolve, reject) => {
setTimeout(() => reject("Ooops!!!"), 2000);
});
let promise3 = new Promise((resolve, reject) => {
setTimeout(() => resolve("I'm Promise 3"), 1000); // takes least time so finishes first
});
Promise.race([promise1, promise2, promise3])
.then((resolver) => console.log(resolver)) // I'm Promise 3
.catch((reject) => console.log(reject));
Here we can see promise3 takes the least time, so it finishes first, thus the output.
Read the previous blog in the series
πGetting Started with JavaScript Modules
I have tried to keep it simple and precise, and if you find any typo/error please report it to me so that I can correct it π
Thanks for reading it till last π
If you find this useful then you can share it with others :)
Let's Connect, drop a Hi and let's chat πππ
Top comments (2)
thank's for the article sir
Thanks for reading sir :)