In this lesson, we will learn about callbacks, promises, and async - await in JavaScript.
You already know how to read contents of a file. Because of the asynchronous nature of JavaScript, we had to pass a function which would get invoked once the file is opened and data is ready.
Let us take another example where image of a user is being displayed. To do that, we will have to:
- Fetch user details.
- Download user image.
- Render the image.
Let us write some JavaScript code to mock these steps. Let's create a file render.js
:
// render.js
const fetchUserDetails = (userID) => {
console.log("Fetching user details");
};
const downloadImage = (imageURL) => {
console.log("Downloading image");
};
const render = (image) => {
console.log("Render image");
};
Now, let's try to make it work. We will first fetch the user details for a user named john, and similar to the file reading example, we will pass a callback function which will have image URL of the user.
const fetchUserDetails = (userID, next) => {
console.log("Fetching user details");
setTimeout(() => {
next(`https://image.example.com/${userID}`);
}, 1000);
};
const downloadImage = (imageURL, next) => {
console.log("Downloading image");
setTimeout(() => {
next(`Data from ${imageURL}`);
}, 1000);
};
const render = (image) => {
setTimeout(() => {
console.log(`Render image: ${image}`);
}, 1000);
};
fetchUserDetails("john", (imageURL) => {
downloadImage(imageURL, (imageData) => {
render(imageData);
});
});
To run the program, execute the following command.
node render.js
You should see an out like the following.
Fetching user details
Downloading image
Render image: Data from https://image.example.com/john
Now, imagine, if you had some other functions like resizing the image, applying some transformation etc, then the sample code would look something like:
fetchUserDetails("john", (imageURL) => {
downloadImage(imageURL, (imageData) => {
resizeImage(imageData, (resizedImage) => {
transformImage(resizedImage, (transformedImage) => {
render(transformedImage);
});
});
});
});
This gets complicated very quickly. This pattern is called Pyramid of doom or a callback hell.
Promises were introduced in ES6 version of JavaScript to make asynchronous code more readable. A Promise
would be in one of the following states:
- pending: initial state, neither fulfilled nor rejected.
- fulfilled: meaning that the operation was completed successfully.
- rejected: meaning that the operation failed.
We can create a new Promise
using the following syntax.
const aPromise = new Promise((resolve, reject) => {
// ...
});
A promise should be either resolved or rejected to proceed further.
The following promise will be resolved after a second.
const aPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Some data");
}, 1000);
});
The following promise will be rejected after a second.
const anotherPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("Server was unreachable!"));
}, 1000);
});
We can either resolve
the promise to change it to fulfilled
state or reject
it to change the state to rejected
. We can chain multiple promises to mimic synchronous behaviour using then
.:
aPromise
.then(handleFulfilledA, handleRejectedA)
.then(handleFulfilledB, handleRejectedB)
.then(handleFulfilledC, handleRejectedC);
We can rewrite our code to use Promise
.
const fetchUserDetails = (userID) => {
console.log("Fetching user details");
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`https://image.example.com/${userID}`);
}, 1000);
});
};
const downloadImage = (imageURL) => {
console.log("Downloading image");
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`Data from ${imageURL}`);
}, 1000);
});
};
const render = (image) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`Render image: ${image}`);
resolve();
}, 1000);
});
};
fetchUserDetails("john")
.then((imageURL) => downloadImage(imageURL))
.then((imageData) => render(imageData))
.catch((err) => {
console.error(err);
})
.finally(() => {
console.log("Done!");
});
We can further simplify the code to be:
fetchUserDetails("john")
.then(downloadImage)
.then(render)
.catch((err) => {
console.error(err);
})
.finally(() => {
console.log("Done!");
});
Any error or rejection of promise will be caught by .catch
block. .finally
gets executed once everything is complete on a promise chain.
The keywords async
and await
were introduced in ECMAScript 2017. It is a syntactic sugar for Promise
. Even though Promise
made code more readable, chaining a lot of them were still tedious.
Keyword async
can only be used with a function declaration. It tells the JS runtime to wrap the function within a Promise
. So a function marked as async
would return a Promise and value would be returned when the promise is fulfilled.
Keyword await
can only be used with a Promise
. It can only be used within a function which is marked as async
. The await
keyword tells the JS runtime to hold the program execution till the promise is resolved or rejected.
We can now rewrite our sample code to use async / await:
const time = async (ms) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, ms);
});
};
const fetchUserDetails = async (userID) => {
console.log("Fetching user details");
await time(1000);
return `https://image.example.com/${userID}`;
};
const downloadImage = async (imageURL) => {
console.log("Downloading image");
await time(1000);
return `Data from ${imageURL}`;
};
const render = async (image) => {
await time(1000);
console.log(`Render image: ${image}`);
};
const run = async () => {
try {
const userDetails = await fetchUserDetails("john");
const imageData = await downloadImage(userDetails);
await render(imageData);
} catch (err) {
console.error(err);
}
};
run();
We have marked functions as async
and we had to write a helper funcion time
to add some delay before the function returns a value.
Also since our functions are async
, we had to write a run
function so that we can wait on the async
functions using the await
keyword.
The code looks like a synchronous one and is much easier to read. Any errors that happen will get thrown and will be caught using the try..catch
block.
The setTimeout()
method calls a function after a number of milliseconds. Syntax for setTimeout
is:
setTimeout(() => {
// callback function
}, milliseconds);
You can learn more about async / await from Mozilla Developer Network