Part 1: A Guide to Callbacks, Promises, and Async/Await in Asynchronous JavaScript

Β·

6 min read

Part 1: A Guide to Callbacks, Promises, and Async/Await in Asynchronous JavaScript

Introduction

When I began my programming journey with JavaScript, one of the first complexities I encountered was its asynchronous behavior. Concepts like callbacks, promises, and async-await in JavaScript were daunting at first, but as I delved deeper, they became fascinating pieces of the asynchronous puzzle.

In this two-part series, we will start by unraveling the mysteries of asynchronous JavaScript, from the foundational use of callbacks to the notorious challenges of callback hell. We'll then explore how promises revolutionized asynchronous operations and how the introduction of async-await has made JavaScript a powerhouse in the realm of asynchronous programming. Let's start this journey to understand and master these crucial aspects of JavaScript.

A Market Adventure: A Metaphor for Synchronous and Asynchronous Processes

Yesterday, two friends, Ram and Shyam, went to the market to complete a few tasks independently. The way they completed these tasks will teach us about an important topic in JavaScript: asynchronous behavior.

Ram's approach

When Ram went to the shopkeeper, he learned that Task 1 would take a few minutes. He waited for it to finish before moving on to Task 2, and then did the same with Task 3. This resulted in Ram taking a lot of time to complete all the tasks, as they were done in a synchronous manner.

Shyam's approach

Unlike Ram, when Shyam discovered that Task 1 would take some extra time, he went to another stall and executed Task 2. By then, Task 1 was ready for execution, which he completed before moving on to Task 3. This way, he was able to finish all the tasks more quickly using an asynchronous approach.

Diving into Asynchronous Javascript

Now that we understood what is asynchronous behaviour, let's see this in action -

function doSomeAsyncTask() {
  setTimeout(() => {
    console.log('Task is now complete after 10 seconds');
  }, 10000)
}

console.log('Task is going to start');
doSomeAsyncTask();
console.log('Task has now ended.');

// Output:
'Task is going to start'
'Task has now ended.'
'Task is now complete after 10 seconds'
// After 10 seconds, this above line was consoled πŸ‘†πŸ‘†πŸ‘†

This is how asynchronous behavior works in JavaScript. When doSomeAsyncTask() takes extra time to execute, it is sent to a queue, and the next line is executed, and when this asynchronous task is completed, it executed. In contrast, if doSomeAsyncTask() were executed synchronously, the next line would have to wait(here 10 seconds) until doSomeAsyncTask() finished executing. This approach is not ideal for creating real-world interactive applications, as they often have several tasks that require time to execute. Blocking the thread would result in the application freezing, which is not desirable.

How to handle asynchronous code in Javascript

Let's imagine we clicked a button to fetch data from an API. Since the API call is asynchronous, the next synchronous line of code executes immediately without waiting for the API call to complete. Once the API call is successful, we need to perform a dependent task, such as rendering the received data to the UI or performing some computation on the newly fetched data.

There are three ways to handle asynchronous tasks in JavaScript, and we'll dive deep into each of them:

Callbacks in Javascript

Callbacks are a kind of function we send as a argument to another function which when completes it's execution executes that callback immediately. In this way, we ensure that once our desired task is completed asynchronously we can perform it's dependent task.

Let's take an example where our API call returns a data in 2 seconds, and we want to console it.

function fetchingApiDataAsync(callback) {
  const data = [1, 2, 3, 4];
  setTimeout(() => {
    callback(data);
  }, 2000)
}

function printApiDataWhenFetched(data) {
  console.log(data);
}

console.log("Application started");
fetchingApiDataAsync(printApiDataWhenFetched)

// Output:
'Application started'
[ 1, 2, 3, 4 ] // after 2 seconds

Here we sent our printApiDataWhenFetched function as a callback inside fetchingApiDataAsync so that as soon as data is fetched asynchronously, we can call our dependent task which is not synchronous in nature.

This is how we handle asynchronous tasks in JS.

Now, suppose there are multiple cases with us, like API can either successfully return the data or fail with some error, all these can be handled using callback.

function fetchDataFromServer(callback) {
  // Simulating a delay like an API call
  setTimeout(() => {
    // Simulating a response
    const response = {
      code: 200, // Change this to a non-200 code to simulate an error
      data: "Fetched Data",
      error: "Error occurred"
    };

    callback(response);
  }, 2000);
}

function fetchingApiDataAsync(success, error) {
  fetchDataFromServer((res) => {
    if (res.code === 200) {
      // API has successfully returned the data
      success(res.data);
    } else {
      // API returned an error
      error(res.error);
    }
  });
}

function printData(data) {
  console.log("Data:", data);
}

function printError(error) {
  console.log("Error:", error);
}

console.log("Application started");
fetchingApiDataAsync(printData, printError);

// Output: 
'Application started'
'Data:' 'Fetched Data' // after 2 seconds

In the code above, we can see how callbacks are used to handle both success and error scenarios.

However, imagine if there is another task 2 dependent on a callback, followed by task 3 dependent on task 2, and so on. This situation, where consecutive callbacks depend on previous ones, creating a chain of callbacks, is commonly known as callback hell. In this case, callbacks are nested within a parent callback and can extend to n-levels deep.

Callback hell

// A callback is a function passed as an argument to another function. This technique allows a function to call another function when a specific task is completed.

function exampleCallback(message, callback) {
  console.log(message);
  callback();
}

exampleCallback("Hello, World!", function() {
  console.log("This is a callback function being executed.");
});

// Callback Hell refers to a situation where callbacks are nested within other callbacks, making the code difficult to read and maintain.

function firstTask(callback) {
  setTimeout(function() {
    console.log("First task completed.");
    callback();
  }, 1000);
}

function secondTask(callback) {
  setTimeout(function() {
    console.log("Second task completed.");
    callback();
  }, 1000);
}

function thirdTask(callback) {
  setTimeout(function() {
    console.log("Third task completed.");
    callback();
  }, 1000);
}

firstTask(function() {
  secondTask(function() {
    thirdTask(function() {
      console.log("All tasks completed. This is an example of callback hell.");
    });
  });
});

In the code snippet above, we can see how challenging it can become when we have multiple tasks depending on one another. This nesting can be difficult to maintain and may lead to bugs if not handled properly.

Before Promises were introduced in ES6, callbacks were our go-to solution for handling all asynchronous tasks.

In the next part of this blog, we'll delve into Promises and async/await.

Summary

In this article, we explored asynchronous behavior in JavaScript. Through the story of Ram and Shyam, we gained an understanding of this concept, as well as the principles of callbacks and the challenges posed by callback hell.

We began by grasping the concept and then delved deeper into asynchronous behavior and how it is managed using callbacks in JavaScript, which in turn helps us create functional applications in this asynchronous world.

In the upcoming second part of the article, we'll examine the remaining two methods of handling asynchronous behavior, i.e using Promises and async/await*.*


🌟 Enjoyed the read? I’d love to hear your thoughts! Share your takeaways and let’s start a conversation. Your insights make this journey richer for all of us.

πŸ‘ If you found this helpful, a clap or two would mean the world β€” it helps more folks find these guides!

πŸ”œ What’s next? I’m already brewing the next topic, packed with more insights and a-ha moments. Stay tuned!

πŸ™ A big thank you for spending time with my words. Your support fuels my passion for sharing knowledge.

πŸ‘‹ Until next time, keep coding and keep exploring!

πŸ’¬ Let’s stay connected! Follow me on Hashnode for more adventures in coding: Rishav Pandey: Hashnode

Β