In JavaScript, we can pass a function to another function as an argument. By definition, a callback is a function that we pass into another function as an argument for executing later.
We are going to focus on Asynchronous callbacks. An asynchronous callback is executed after the execution of the high-order function that uses the callback.
Asynchronicity means that if JavaScript has to wait for an operation to complete, it will execute the rest of the code while waiting.
Here is an example of Asynchronous callback;
Suppose that you need to develop a script that downloads a picture from a remote server and process it after the download completes:
function download(url) {
// ...
}
function process(picture) {
// ...
}
download(url);
process(picture);
However, downloading a picture from a remote server takes time depending on the network speed and the size of the picture.
The following download()
function uses the setTimeout()
function to simulate the network request:
function download(url) {
setTimeout(() => {
// script to download the picture here
console.log(`Downloading ${url} ...`);
},1000);
}
And this code emulates the process()
function:
function process(picture) {
console.log(`Processing ${picture}`);
}
When we execute the following code:
let url = 'https://www.foot.net/pic.jpg';
download(url);
process(url);
we will get the following output:
Processing https://foo.net/pic.jpg
Downloading https://foo.net/pic.jpg ...
This is not what we expected because the process()
function executes before the download()
function. The correct sequence should be:
- Download the picture and wait for the download completes.
- Process the picture.
To resolve this issue, we can pass the process()
function to the download()
function and execute the process()
function inside the download()
function once the download completes, like this:
function download(url, callback) {
setTimeout(() => {
// script to download the picture here
console.log(`Downloading ${url} ...`);
// process the picture once it is completed
callback(url);
}, 1000);
}
function process(picture) {
console.log(`Processing ${picture}`);
}
let url = 'https://wwww.javascripttutorial.net/pic.jpg';
download(url, process);
Output:
Downloading https://www.foo.net/pic.jpg ...
Processing https://www.foo.net/pic.jpg
Now, it works as expected.
In this example, the process()
is a callback passed into an asynchronous function.
When we use a callback to continue code execution after an asynchronous operation, the callback is called an asynchronous callback.
To make the code more concise, we can define the process()
function as an anonymous function:
function download(url, callback) {
setTimeout(() => {
// script to download the picture here
console.log(`Downloading ${url} ...`);
// process the picture once it is completed
callback(url);
}, 1000);
}
let url = 'https://www.javascripttutorial.net/pic.jpg';
download(url, function(picture) {
console.log(`Processing ${picture}`);
});
Handling errors
The download()
function assumes that everything works fine and does not consider any exceptions. The following code introduces two callbacks: success
and failure
to handle the success and failure cases respectively:
function download(url, success, failure) {
setTimeout(() => {
console.log(`Downloading the picture from ${url} ...`);
!url ? failure(url) : success(url);
}, 1000);
}
download(
'',
(url) => console.log(`Processing the picture ${url}`),
(url) => console.log(`The '${url}' is not valid`)
);
Nesting callbacks and the Pyramid of Doom
How do we download three pictures and process them sequentially? A typical approach is to call the download()
function inside the callback function, like this:
function download(url, callback) {
setTimeout(() => {
console.log(`Downloading ${url} ...`);
callback(url);
}, 1000);
}
const url1 = 'https://www.foo.net/pic1.jpg';
const url2 = 'https://www.foo.net/pic2.jpg';
const url3 = 'https://www.foo.net/pic3.jpg';
download(url1, function (url) {
console.log(`Processing ${url}`);
download(url2, function (url) {
console.log(`Processing ${url}`);
download(url3, function (url) {
console.log(`Processing ${url}`);
});
});
});
Output:
Downloading https://www.foo.net/pic1.jpg ...
Processing https://www.foo.net/pic1.jpg
Downloading https://www.foo.net/pic2.jpg ...
Processing https://www.foo.net/pic2.jpg
Downloading https://www.foo.net/pic3.jpg ...
Processing https://www.foo.net/pic3.jpg
The script works perfectly fine.
However, this callback strategy does not scale well when the complexity grows significantly.
Nesting many asynchronous functions inside callbacks is known as the pyramid of doom or the callback hell:
asyncFunction(function(){
asyncFunction(function(){
asyncFunction(function(){
asyncFunction(function(){
asyncFunction(function(){
....
});
});
});
});
});
To avoid the pyramid of doom, we use promises or async / await functions.