This content is for members only.
Already have an account? Login
Javascript is the most famous and widely used programming language in web development community. It was ranked #1 in the 2025 Develop Survey by Stackoverflow (Link to Survey) which asked 30,000+ respondents about the programming, scripting, and markup languages they have used in the past year or plan to use in the next year. One of the reasons developers love and prefer to use Javascript over most other scripting languages is due its speed and ability to run asynchronous code very quickly. But what is asynchronous code?
By default, all programming languages (including Javascript) are synchronous, which means they execute (or run) the code line by line consecutively in a sequential manner. The execution of one statement must finish before the execution of next statement can begin. Asynchronous execution allows multiple statements to be executed concurrently (in parallel) without any waiting. This is useful during many common web development scenarios like when loading a webpage and fetching data from a database at the same time.
Javascript allows 3 common ways for asynchronous code to execute: Callbacks, Promises, and Async/await. Let's understand these one by one.
1. Callbacks
Consider the following (synchronous) code which prints 3 statements to the console:
console.log("Hello World");
console.log("I'm learning about Callbacks");
console.log("Callbacks are fun");If you run this code in Javascript, the compiler will first execute statement 1 and print 'Hello World' to the console. Once that is done, then it executes statement 2 and once that finishes then finally statement 3 will get executed. There are many ways to convert this synchronous code into asynchronous code. Let's start with a simple way using the setTimeout() function. Consider the following (asynchronous) code:
console.log("Hello World");
setTimeout(() => {
console.log("I'm learning about Callbacks");
}, 5000);
console.log("Callbacks are fun");In this code, once the compiler finishes execution of statement 1, it sees the setTimeout() function. The setTimeout() function is used to introduce a time delay in the execution of some code. It requires two arguments: a function to execute once the timer is complete and amount of time to wait (in milliseconds) before executing that function. In this case, we have passed an anonymous function and 5000 milliseconds to setTimeout() therefore it will execute that anonymous function after 5000 milliseconds (or 5 seconds) of delay.
But since setTimeout() works asynchronously, the compiler starts the timer and immediately moves to the next statement. Therefore, it prints the statement 3 and finally when the timer expires (5 seconds later), the code in the anonymous function is executed and compiler prints the statement on the console.
The anonymous function passed to the setTimeout() is sometimes called a callback function since it is called (executed) after certain amount of time. Another common example of callback function is eventListeners which wait for a certain event (like a button click) asynchronously (independent from other code) and then run the callback function once the event is triggered.
1.1 Error First Callbacks
Error first callbacks extend the functionality of callbacks to include error handling. When a callback is executed and it results in an error we may want to handle that error using code that is separate from normal error-free code and error first callbacks allow us to do exactly that.
Consider the readFile() method from "fs" module in Javascript which is used to read files from the current directory. The readFile() method needs 3 arguments: name of the file, encoding of the file, and a callback function that takes two properties β error and data.
const fs = require('fs');
fs.readFile('./text.txt', {encoding: 'utf-8'}, (err, data) => {
if (err){
console.error("An error occoured while reading file!");
}else{
console.log("File read successfully!");
}
});In this case, you can see that our callback takes 2 arguments βerr and data. If an error occurs during the file read, the callback is called with the the "err" property set so that it contains the error information. In this case the "data" will be undefined. On the other hand, if no error occurs, the callback will be called with "data" property set so that it contains the data and the "err" will be undefined. Inside the callback, we can examine whether or not an error occurred by reading the values of the "err" or "data" property and take appropriate actions.
In this way, error first callbacks allow us to handle errors that occur during the execution of our asynchronous code.
2. Promises
Promises are very similar to error first callbacks in the way that they allow us to run asynchronously code while still being able to handle any errors that occur during the execution of the code. The only difference is that promises allow us to separate that asynchronous code (which may or may not result in an error) from the code that handle the completion/failure of asynchronous code. This separation is achieved by using the .then() and .catch() handlers along with resolve/reject arguments that are provided with Promises.
For example consider this very simple Promise which generates a random number between 0 and 2 and finds the floor of the number (greatest integer less than or equal to the number). If the floor is 0 then we call the resolve function to tell the Promise that no error occurred and our code executed successfully. If the floor if 1 then we call the reject function which tells the Promise that an error occurred and we need to handle it.
const myPromise = new Promise((resolve, reject) => {
const rand = Math.floor(Math.random() * 2);
if (rand == 0){
resolve();
}else{
reject();
}
});If we run this code, nothing will happen. This is because we have just created a Promise but we haven't added the part which actually executes the Promise and then handles the error/no-error case. You can see we have stored the promise in a variable myPromise. To execute it, we use the .then() and .catch() handlers as shown below.
myPromise.then(() => {
console.log("Success");
}).catch(() => {
console.error("An Error occurred!");
});If the execution of the promise leads to an error, the .catch() part of this code will run and we will output 'An Error occurred' to the console. Similarly if the execution leads to no error, we run the .then() part of the code and log 'Success' message to the console. In this way, promises allow us to separate the asynchronous code from the code that handle the completion/failure of the asynchronous code.
Let's demonstrate the same read file task with Promises. The fs module in Javascript comes with a set of Promises that we can use to read file.
// Importing the Module
const fs = require('fs');
// Calling the Promise
fs.promises.readFile('./text.txt', {encoding: 'utf-8'}).then(() => {
console.log("Success");
}).catch(() => {
console.error("An error occoured");
});In the above code, we are using the readFile() method from fs.promises. This method returns a Promise and so to execute that promise, we call the .then() method, followed by .catch() method to catch any errors. The .catch() method is not mandatory after .then() but is just a good habit if you want to write code that handles errors but the .then() method is required otherwise the asynchronous code inside the Promise will not be executed.
3. Async / await
Async / await takes asynchronous code execution from Promises and Callbacks a step forward. It not only improves the readability of code but also makes it more natural to standard way of writing and calling function. So far with callbacks and Promises, we were always having to define an anonymous function which executed our code and then based on it's execution we were handling the rest. With async await, we no longer have to create these anonymous functions as the result of the asynchronous code is available to us in a more natural form.
Consider a situation where you have to use the same readFile promise from "fs.promises" class in your code (say within some readFile function). With promises we had use .then() and .catch() methods but with async/await we can just call the readFile function using the await keyword and store the data in a variable (say data).
// Importing the Module
const fs = require('fs');
async function readFile(){
const data = await fs.promises.readFile('./text.txt', {encoding: 'utf-8'});
console.log(data);
};This is much more simpler and natural to how synchronous code executes. Notice the await keyword before the fs.promises.readFile() method. It tells the compiler that the following is asynchronous code and needs to be run asynchronously. Note: The await keyword must always be inside a function that has the async keyword. This can be easily handled in your own code by writing the async keyword before the function declaration like we have shown above. Now you might wonder what if an error occurred here, how do we handle that? That's where async/await uses the try-catch block. We can wrap the entire code in a try-catch block like so
// Importing the Module
const fs = require('fs');
async function readFile(){
try {
const data = await fs.promises.readFile('./text.txt', {encoding: 'utf-8'});
console.log(data);
} catch (error) {
console.error("An error occoured!");
}
};This tells the compiler that we want to run asynchronous code and if it results in an error handle it in the same way you handle errors for synchronous code. This makes the integration of asynchronous code with synchronous code much more natural and also improves readability for other developers.
That it for this one! Asynchronous Javascript is one of the most important (and also most challenging) topics in Javascript but it is very rewarding once you get it right. I hope this tutorial was able to help you understand this intricate topic a little better.
A lot of the content for this post is inspired from this amazing video by James Q Quick on YouTube.
If you have any questions or suggestions on this topic, please do let us know in the comment and we'll be happy to help you out. Stay safe!
