By default, a JavaScript program is executed synchronously, meaning it will wait for the previous line to finish before moving on to the next. For example,
1function task1() {
2 console.log("Task 1 finished.");
3}
4
5function task2() {
6 console.log("Task 2 finished.");
7}
8
9function task3() {
10 console.log("Task 3 finished.");
11}
12
13task1();
14task2();
15task3();
1Task 1 finished.
2Task 2 finished.
3Task 3 finished.
The three tasks will be executed according to the sequence they are called.
However, imagine task number 2 is something that requires a long time. For demonstration purposes, we are using a for
loop to count to 9,000,000,000
, which should take a few seconds on your computer.
1function task1() {
2 console.log("Task 1 finished.");
3}
4
5function task2() {
6 for (let i = 0; i < 9000000000; i++) {} // This loop should take a few seconds to finish
7 console.log("Task 2 finished.");
8}
9
10function task3() {
11 console.log("Task 3 finished.");
12}
13
14task1();
15task2();
16task3();
Run this example code and notice that the tasks are still being executed based on the order in which they are called. This means task2()
will be blocking the execution of task3()
. But from our example, you can see that task3()
does not depend on task2()
, so this way of doing things is not very effective.
Instead, what if you could allow multiple tasks to run in parallel? For example, maybe in this case, you could enable task3()
to execute while waiting for task2()
.
Asynchronous programming
This is the concept of asynchronous programming. It is a programming technique that allows you to start a time-consuming task in parallel with other easier tasks, so that your program is still responsive.
In JavaScript, this can be achieved using a special type of function called asynchronous functions. They always accept a callback function as the input. That callback function will be taken out of the normal execution sequence of the program, so that the rest of the code can execute as usual.
That callback function will be executed at a later time, or when certain conditions are satisfied.
A typical example of an asynchronous function would be setTimeout()
. It accepts a callback function and a numeric value indicating the number of milliseconds you want the callback to be delayed. This setTimeout()
function does not block the execution of the rest of the code when it is waiting. For instance:
1task1();
2
3setTimeout(task2, 1000);
4
5task3();
This time, instead of running task2()
directly, we are passing it to setTimeout()
as a callback. And because setTimeout()
is asynchronous, it will allow task3()
to be executed first.
1Task 1 finished.
2Task 3 finished.
3Task 2 finished.
Use cases for asynchronous programming
When working on a web application, you will often encounter situations where you need to implement asynchronous programming techniques, both in the frontend and the backend.
For example, if you are reading a file in the backend, which is likely a very time-consuming task, especially when you are dealing with large files. You don't want this action to block the execution of the rest of your code.
Node.js offers a file system library called fs
, which includes an asynchronous readFile()
function. This function allows you to perform other actions while the file is being loaded.
1const fs = require("fs");
2
3console.log("Hello!");
4
5fs.readFile("file.txt", "utf8", function (err, data) {
6 console.log(data);
7});
8
9console.log("Bye!");
require()
is how we can include external libraries in JavaScript programs. We will discuss more about it later.
Assuming you have a file.txt
file in your work directory.
1.
2├── file.txt
3└── index.js
This example will give the following output:
1Hello!
2Bye!
3Lorem ipsum, dolor sit amet consectetur adipisicing elit. Magnam sint iusto modi blanditiis eos id dolore consectetur facere quisquam impedit architecto magni repudiandae necessitatibus veniam illo fugiat enim, dolorem error?
Asynchronous programming is a technique commonly used in the frontend as well. Later, we are going to introduce a concept called event handling, which is a special type of asynchronous function similar to setTimeout()
, except it will execute the callback function when a certain user action is received, such as click, scroll, hover, and so on.
The callback pyramid of doom
An asynchronous function always accepts a callback, which is a function that is passed to another function as its input argument. It is a function that is not executed immediately, but "calls back" later when certain conditions are satisfied, hence its name, the callback function.
There is one thing you must pay attention to when working with callbacks, you must pass the entire function to the parent function, not executing it. For instance, in our previous example, notice that we are passing task2
as the callback, not task2()
. The latter will execute the function.
1setTimeout(task2, 1000);
This callback style of writing asynchronous programs is surely a workable way, but as the program gets more complex, it becomes very difficult to maintain and debug. As an example, here is a function that compares two files using readFile()
.
1function compareFiles(f1, f2) {
2 fs.readFile(f1, "utf8", (error, data1) => {
3 fs.readFile(f2, "utf8", (error, data2) => {
4 if (data1 === data2) {
5 console.log("Files are the same.");
6 } else {
7 console.log("Files are not the same.");
8 }
9 });
10 });
11}
To compare two files, we have to pass one callback into another callback function. Imagine we are comparing three, four, and even more files, we'll need even more layers of callbacks. This is sometimes referred to as a "callback pyramid of doom", because it looks like a pyramid on its side.
Obviously, we are going to need a better and easier way of doing things. Maybe instead of passing data around in layers and layers of callback functions, we can perhaps make the asynchronous function return something like a regular function, and the returned value can be passed to a variable like this:
1let variable = readFile("file.txt");
This is a syntax we are familiar with, and it is much easier to understand.
However, there are some problems we have to solve. What if the action fails? What if readFile()
cannot parse the specified file? There might be other programs that are excepting our asynchronous function to give a valid response. What if it returns something that future programs cannot work with?
To achieve this goal, we must first discuss promises.