Understanding Async Javascript: An easy-to-understand guide

Understanding Async Javascript: An easy-to-understand guide

One of the most crucial concepts of Javascript, Async/Await has left many newbie developers scratching their heads. In this blog, you will learn what asynchronous javascript is, why is it necessary, and how to use it. Let's dive in.

What is synchronous programming in the first place?

Imagine you and your friend want to finish a task together. You start the work while your friend waits, when your part is done, your friend starts the work while you wait. So the work is done irrespective of who is doing it. This pretty much is the jist of synchronous programming. Let us understand this with an example.

function John(word) {
  console.log('John says: ' + word);
}
function Smith(word) {
  console.log('Smith says: ' + word);
}
function conversation() {
  playerOneSays('Hello');
  playerTwoSays('Hi');
  playerOneSays('How are you?');
  playerTwoSays('I am fine, thank you!'); // Both actors saying their 
                                          // words one after the other
}
playGameSynchronously();

In the above code block, John and Smith both work together doing their part and the result is a continuous conversation. This is the synchronous style of programming. This comes with a big problem. If John takes too long to complete his part then Smith will have to wait until John finishes. This causes delays and performance issues, none of which is desirable in modern-day applications.

What is Asynchronous Programming?

Asynchronous programming is a method that allows your program to start executing a long task and still be able to respond to other events. Think of it as multitasking. Asynchronous Programming isn't anything new. It has been commonly used for tasks such as handling AJAX requests, file handling, managing timeout, and so many other things. Let us look at a simple async function in javascript.

async function func(){
    return "I LOVE JS"
}
func().then(
    func(value){
    display(value)// to display the desired value
}
    func(error){
    display(error)// to display the error in case of one
}
)

In this program, the function func() is declared as an async function indicating that it will return a promise. Now a promise is not a guarantee that the desired output will be achieved. You will learn more about promises at a later part of this blog but for now, just understand that a promise can have 3 states and represents a value that might be available now (resolved), available in the future(pending), or not at all(rejected).

Since the func() function is referred to as async, which means that it will return a promise that will resolve with the value "I LOVE JS". Now the then() method will be used on the promise returned by func(). It takes two arguments - the first of which is a callback function that will be executed when the promise is resolved and the second argument is also a callback function that will be executed if the promise is rejected, in this case, an error is thrown.

Event Handlers

An event handler is a function that is used to respond to a specific event. These events include user interactive elements such as clicking a button, submitting a form, and movement of the mouse among other things. Event handlers are actually a form of asynchronous javascript. A function i.e. the event handler is called when the specified event occurs. Let us look at an example.

const button = document.getElementById('myButton');
button.addEventListener('click', () => {
  console.log('Button clicked!');
});

In the above code block, the button is hooked to the 'myButton' id, and upon a click, its logs 'Button Clicked' to the console. The event handler function is called when the user clicks the button, and it utilizes asynchronous JavaScript to handle the data fetching and console logging the output. This asynchronous behavior allows the browser to continue processing other tasks while waiting for the async operation to complete.

Callbacks

A callback is simply a function that is passed into a function. In a way, event handlers are a type of callback since they are passed onto the function they are expected to affect on. Callbacks are used almost everywhere one can imagine. However, the problem arises when one callback has to call a function that accepts a callback. Think of it like a callback including another callback as its function parameter. This quickly turns into a nested callback structure, popularly known as Callback Hell. Let us try to understand it with an example.

async1(function(result1) {
    async2(result1, function(result2) {
        async3(result2, function(result3) {
            async4(result3, function(result4) {
            });
        });
    });
});

In the above code block, each async operation is asynchronous and we need to wait for its completion before performing the next operation. As the number of these nested callback grows, our code becomes more repetitive and harder to understand. Now imagine what an error at function(result1) would do to the rest of the code. This shortcoming of callbacks is mitigated using promises.

Promises

A promise is an object returned by an asynchronous function. When a promise is returned to the caller, it does not indicate that the task has been completed. It rather provides the methods to handle the eventual success or failure of that operation. Promises help avoid callback hell by allowing the user to chain asynchronous functions together and handle success and error cases more efficiently. Let us understand promises better with an example.

function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const data = { message: "Data successfully fetched!" };
            // Simulate a successful response
            resolve(data);
            reject(new Error("Failed to fetch data"));
        }, 1000); 
    });
}

In the above code block, the fetchData function returns a promise that simulates fetching data in an async fashion. After a delay of 1 second, the function either resolves with successful data or rejects with an error. To support error handling, Promise objects provide a catch() method. This however only catches the errors that occur within the corresponding asynchronous function.

Async and await

As we have seen at the start of this guide, the async keyword is used to make any function asynchronous. Inside the async function, we can use the await keyword. Now what this does is makes the code wait at that point until the promise is resolved. This enables us to write asynchronous functions that look like synchronous code. It is however important to note that we can only use the await keyword inside and async function. Let us understand it better with the help of an example of a weather API fetch call.

function fetchWeather(city) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const weatherData = {
        city: city,
        temperature: 25,
        conditions: 'Sunny'
      };
      resolve(weatherData);
    }, 2000);
  });
}
async function getWeatherReport(city) {
  try {
    const weather = await fetchWeather(city);
    console.log(`Weather in ${weather.city}: ${weather.temperature}°C, ${weather.conditions}`);
  } catch (error) {
    console.error("Error fetching weather:", error);
  }
}
getWeatherReport("New York");
console.log("Fetching weather data...");

Now, let's delve deeper into what's happening in the getWeatherReport example.

  1. The fetchWeather() function simulates an asynchronous API call using a Promise. It returns a promise that represents a future value – the weather data. The resolve function is used to fulfill this promise, which happens after a 2-second delay.

  2. The getWeatherReport() function is declared as an async function. This designation allows us to utilize the await keyword inside it. The await keyword essentially pauses the execution of the function until the promise from fetchWeather() is resolved. This means that the function won't proceed until it has the necessary weather data.

  3. Within the try block, we handle the successful response from the API call. By using await, the code waits for the promise from fetchWeather() to resolve, ensuring that the weather data is available. Once the promise resolves, the weather data is logged to the console. If an error occurs during this process, it's caught by the catch block.

  4. Imagine you call getWeatherReport('Kolkata'). This initiates an asynchronous process. The function waits for the data to be fetched before it proceeds. If the data is successfully fetched, the resolved promise delivers the weather information to the weather variable, and the function logs it. On the other hand, if there's an issue during the data fetching process, such as a network error, the catch block captures the error and logs an error message.

By utilizing async/await, we can structure our asynchronous code in a more intuitive and readable manner, making it easier to understand and maintain.

Conclusion

Promises are the building blocks of asynchronous javascript. They make it easier to sequence asynchronous operations without having to deal with callback hell. The async/wait keywords make it easier to build an operation from a series of asynchronous calls allowing us to write code that looks just like synchronous code but functions asynchronously. By understanding these concepts, you will be better equipped to create meaningful javascript applications.

Did you find this article valuable?

Support Anubhav Adhikari by becoming a sponsor. Any amount is appreciated!