How to Return Response From an Asynchronous Call in JavaScript

In this blog, we will be writing script for this app using async/await and promise .then syntax . We will also write scripts for other apps as well. Keep on reading as this article is written in depth. We will first understand what is asynchronous call and callback hell. Then we will discuss different approaches to write asynchronous code.

You can try using the app.

Disclaimer: This application is built for educational purposes. Jokes are not filtered, and we are not responsible if any joke offends anyone. The random joke is fetched from the third party API.

Asynchronous call in programming is a way of doing tasks without waiting for them to finish before moving on to the next task. Instead of waiting, the program can continue doing other things and get back to the task when it’s done.

For example, let’s say you want to fetch some data from a remote server. In a synchronous scenario, the program would make the request and wait until it receives the data before doing anything else. But with an asynchronous call, the program can make the request and move on to other tasks without waiting. Once the data is ready, the program will be notified and can handle the data.

Asynchronous calls are often used for tasks like making network requests, reading/writing files, working with databases, or doing computations that take a long time. By using asynchronous calls, programs can be more efficient, responsive, and able to handle multiple tasks at the same time.

In JavaScript, asynchronous calls are commonly managed using techniques like callbacks, promises, or the newer async/await syntax. These techniques provide ways to handle the asynchronous response and ensure that the program flows smoothly even when dealing with time-consuming tasks.

Callback Hell :

Now, let’s talk about “callback hell.” It refers to a situation where you have multiple nested callbacks in your code, making it difficult to read and understand. Here’s an example:

asyncOperation1(arg1, (error1, result1) => {
  if (error1) {
    console.error(error1);
  } else {
    asyncOperation2(arg2, (error2, result2) => {
      if (error2) {
        console.error(error2);
      } else {
        asyncOperation3(arg3, (error3, result3) => {
          if (error3) {
            console.error(error3);
          } else {
            // Continue with more nested callbacks...
          }
        });
      }
    });
  }
});

In this example, there are three asynchronous operations (asyncOperation1, asyncOperation2, and asyncOperation3) that depend on each other. As a result, the callbacks are nested inside each other, creating a deep indentation and making the code hard to follow.

Each callback checks for errors and handles them if they occur. If there are no errors, it moves on to the next asynchronous operation, leading to more levels of nesting. This nesting can quickly become overwhelming and make the code difficult to manage and maintain.

Callback hell can make code harder to debug, test, and understand, which leads to poor code quality. It lacks the readability and clarity provided by Promise-based or async/await syntax, which offer more structured and sequential ways to handle asynchronous operations.

To mitigate callback hell, you can use Promises or async/await syntax, as will be shown in the upcoming examples. These approaches provide cleaner and more organized ways to handle asynchronous code, making it easier to read, write, and maintain.

Promises :

Let’s use the JokeAPI to get a random joke as an example.

function fetchRandomJoke() {
  return new Promise((resolve, reject) => {
    fetch('https://v2.jokeapi.dev/joke/Any')
      .then(response => {
        if (response.ok) {
          return response.json();
        } else {
          throw new Error('Network response was not ok');
        }
      })
      .then(data => resolve(data))
      .catch(error => reject(error));
  });
}
fetchRandomJoke()
  .then(joke => {
    console.log(joke.setup); // Access the joke setup
    console.log(joke.delivery); // Access the joke delivery (for jokes with two parts)
  })
  .catch(error => {
    console.error(error); // Handle any errors that occurred
  });

The fetch() function is used to fetch data from a specific URL (https://v2.jokeapi.dev/joke/Any). In this case, we’re fetching a random joke from the JokeAPI.

After making the request, we use the .then() method to handle the response. Inside the .then() block, we check if the response is okay by evaluating response.ok. If the response is okay (status code 200-299), we call response.json(), which returns another promise that resolves to the parsed JSON data from the response body. If the response is not okay, we throw an error using throw new Error('Network response was not ok'). This error will be caught in the subsequent .catch() block.

The second .then() block is chained to the first one and receives the parsed JSON data as a parameter. It resolves the promise by calling resolve(data), passing the data as the resolved value. This allows the data to be accessed in the next .then() block when using the fetchRandomJoke() function.

The .catch() block is used to handle any errors that occurred during the API request or JSON parsing. It receives the error object as a parameter and rejects the promise by calling reject(error). This allows the error to be handled in the .catch() block when using the fetchRandomJoke() function.

Let’s take another example of reading the content of a file asynchronously and displaying it:

We will use the built-in JavaScript FileReader API. This simple app allows you to select a text file/code files from your computer and display its content. Save the following code in an HTML file and open it in your browser.

<!DOCTYPE html>
<html>
<head>
  <title>File Reader Example</title>
  <style>
    pre {
      white-space: pre-wrap;
    }
  </style>
</head>
<body>
  <input type="file" id="file-input">
  <pre id="file-content"></pre>
  <script>
    function readFile(file) {
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = event => resolve(event.target.result);
        reader.onerror = error => reject(error);
        reader.readAsText(file);
      });
    }
    const fileInput = document.querySelector('#file-input');
    const fileContentPre = document.querySelector('#file-content');
    fileInput.addEventListener('change', event => {
      const file = event.target.files[0];
      readFile(file)
        .then(content => {
          fileContentPre.textContent = content; // Display the file content
        })
        .catch(error => {
          console.error(error); // Handle any errors that occurred
        });
    });
  </script>
</body>
</html>

The <input type="file" id="file-input"> element is an HTML file input control. It allows the user to select a file from their computer.

The JavaScript code is placed within the <script> tags. It defines the readFile function and sets up an event listener for the change event on the file-input element.

When you open this HTML file in a web browser, you will see a file input control on the page. Clicking on the file input control will open a file selection dialog, allowing you to choose a file from your computer.

Once you select a file, the change event will be triggered, and the event listener attached to the file-input element will handle the event. The selected file will be passed to the readFile function, which will read the content of the file using a FileReader. Once the content is read, it will be displayed in the <pre> element with the id file-content, allowing you to see the content of the file. You can also log the content of the file in the console.

Simulating a network request with a random delay:

function simulateNetworkRequest() {
    const delay = Math.random() * 3000 + 1000; // Random delay between 1 and 4 seconds
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (Math.random() < 0.5) {
          resolve({ message: 'Request succeeded', delay });
        } else {
          reject(new Error(`Request failed. Delay: ${delay}`));
        }
      }, delay);
    });
  }
  simulateNetworkRequest()
    .then(response => {
      console.log(response.message);
      console.log('Delay:', response.delay);
    })
    .catch(error => {
      console.error(error);
      if (error.message.includes('Delay')) {
        const delay = error.message.match(/Delay: (\d+)/);
        if (delay) {
          console.log('Delay:', delay[1]);
        }
      }
    });

Depending on the random number value the response is set , if random number is less than 0.5 the response will be set with the object { message: 'Request succeeded', delay }. otherwise `Request failed. Delay: ${delay}`.

setTimeout function is a JavaScript method that allows you to schedule the execution of a function after a specified delay. It is commonly used for delaying the execution of code or performing an action after a certain amount of time has passed.

Using async/await for asynchronous call:

Now let’s get back to each of the examples and write using async/await .

lets first discuss joke api.

async function fetchRandomJoke() {
  try {
    const response = await fetch('https://v2.jokeapi.dev/joke/Any');
    if (response.ok) {
      const data = await response.json();
      return data;
    } else {
      throw new Error('Network response was not ok');
    }
  } catch (error) {
    throw error;
  }
}
//async await in IIF (immediately invoked function )
(async () => {
  try {
    const joke = await fetchRandomJoke();
    console.log(joke.setup); // Access the joke setup
    console.log(joke.delivery); // Access the joke delivery (for jokes with two parts)
  } catch (error) {
    console.error(error); // Handle any errors that occurred
  }
})();
// awync await in regular function.
async function run() {
  try {
    const joke = await fetchRandomJoke();
    console.log(joke.setup); // Access the joke setup
    console.log(joke.delivery); // Access the joke delivery (for jokes with two parts)
  } catch (error) {
    console.error(error); // Handle any errors that occurred
  }
}
run();
//async await in arrow function.
const execute = async () => {
  try {
    const joke = await fetchRandomJoke();
    console.log(joke.setup); // Access the joke setup
    console.log(joke.delivery); // Access the joke delivery (for jokes with two parts)
  } catch (error) {
    console.error(error); // Handle any errors that occurred
  }
};
execute();

I used three formats of function that is commonly used to demonstrate how you can use async/await with each. So, this time you will see three jokes displayed in console.

await keyword is used to wait for the fetchRandomJoke function to resolve with the joke data. When the await keyword is used before a promise, it pauses the execution of the program at that point until the promise is resolved or rejected. In the case of await fetchRandomJoke(), the execution of the program will be halted at that line until the fetchRandomJoke function completes and resolves with the joke data.

While the fetchRandomJoke function is executing and waiting for the fetch request to complete, the control is temporarily returned to the caller of the execute function. The program execution will continue with the next line only after the fetchRandomJoke promise is resolved and the joke data is available. This behavior allows to write asynchronous code in a more synchronous-like manner.

You may think how does this async/await syntax is asynchronous call , when await halts the execution of the program till promise resolves?

await keyword in async/await does pause the execution of the program until the awaited promise is resolved or rejected. This behavior is often referred to as blocking or synchronous behavior.

In the context of async/await, the “asynchronous” aspect comes from the fact that the underlying operations, such as network requests or file I/O, are inherently asynchronous. When using async/await, the program flow appears to be synchronous, as the code is written in a linear fashion without the need for callbacks or explicit chaining of .then() methods. However, internally, the JavaScript engine handles the asynchronous nature of the underlying operations.

Although the execution appears to be blocked at the await statement, the JavaScript engine is not idle. It continues executing other tasks, such as handling user input or processing events, while it waits for the awaited promise to resolve. Once the promise is resolved, the program execution resumes from the point where it left off.

async/await simplifies the writing and understanding of asynchronous code by providing a more synchronous-like syntax. While the program execution appears to be halted at the await statement, the overall execution of the program is still asynchronous, as it allows other tasks to be performed while waiting for the awaited promise to complete.

When using a chain of .then() methods, you are working with Promises and utilizing the Promise-based syntax. Promises allow you to handle asynchronous operations in a sequential manner by chaining multiple .then() methods. Each .then() method in the chain represents a subsequent step that will be executed when the previous Promise resolves.

The important distinction is that a chain of .then() methods is non-blocking, which means that the program execution does not pause or wait for each Promise in the chain to resolve. Instead, the Promises are scheduled to execute in the event loop, and the program continues executing the subsequent code immediately after the .then() method.

In contrast, async/await provides a more synchronous-like approach to working with Promises. When you await a Promise, it halts the execution of the current async function until the Promise is resolved. While the program is waiting for the Promise to resolve, it can perform other tasks or handle other events. The await keyword essentially provides a syntactical sugar for writing asynchronous code in a more readable and sequential manner.

    Chain of .then() methods: Non-blocking, allows you to handle Promises sequentially, but the program execution continues immediately after each .then() method without waiting for the Promise to resolve.

    async/await: Blocking, allows you to write asynchronous code in a more synchronous-like manner, where the execution of the program pauses at await until the awaited Promise is resolved.

Now that you have good understand of async/await syntax ,can you try to write the other two examples using async/await. you can read the solution below..

Reading the content of a file asynchronously and displaying it (using async/await syntax):

<!DOCTYPE html>
<html>
<head>
  <title>File Reader Example</title>
  <style>
    pre {
      white-space: pre-wrap;
    }
  </style>
</head>
<body>
  <input type="file" id="file-input">
  <pre id="file-content"></pre>
  <script>
    function readFile(file) {
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = event => resolve(event.target.result);
        reader.onerror = error => reject(error);
        reader.readAsText(file);
      });
    }
    const fileInput = document.querySelector('#file-input');
    const fileContentPre = document.querySelector('#file-content');
    fileInput.addEventListener('change', async event => {
      const file = event.target.files[0];
      try {
        const content = await readFile(file);
        fileContentPre.textContent = content; // Display the file content
      } catch (error) {
        console.error(error); // Handle any errors that occurred
      }
    });
  </script>
</body>
</html>

In this code, the event listener function for the change event is now marked as async. This allows us to use await inside the function. The readFile function remains the same.

Inside the change event listener, the readFile(file) call is now awaited, and the result is stored in the content variable. The file content is then assigned to the textcontent property of the filecontentPre element, displaying the file content in the <pre>element.

Any errors that occur during the file reading process are caught using a try/catch block, and the error is logged to the console.

Simulating a network request with a random delay (using async/await syntax):

function simulateNetworkRequest() {
  const delay = Math.random() * 3000 + 1000; // Random delay between 1 and 4 seconds
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() < 0.5) {
        resolve({ message: 'Request succeeded', delay });
      } else {
        reject(new Error(`Request failed. Delay: ${delay}`));
      }
    }, delay);
  });
}
async function executeNetworkRequest() {
  try {
    const response = await simulateNetworkRequest();
    console.log(response.message);
    console.log('Delay:', response.delay);
  } catch (error) {
    console.error(error);
    if (error.message.includes('Delay')) {
      const delay = error.message.match(/Delay: (\d+)/);
      if (delay) {
        console.log('Delay:', delay[1]);
      }
    }
  }
}
executeNetworkRequest();

In this updated code, the simulateNetworkRequest function remains the same. However, we’ve introduced a new executeNetworkRequest function that is marked as async. This allows us to use await inside this function.

Inside the executeNetworkRequest function, we use a try/catch block to handle any errors that may occur during the execution of simulateNetworkRequest. We await the simulateNetworkRequest function call, and the resolved value is stored in the response variable. If the promise resolves successfully, we log the response.message and the response.delay to the console.

If an error occurs, we log the error to the console. Additionally, we check if the error message includes the word ‘Delay’. If it does, we extract the delay value using a regular expression and log it to the console as well.

Finally, we call the executeNetworkRequest function to start the execution of the network request.

Lets summarize the whole article.

    Promises: Promises provide a way to handle asynchronous operations by chaining .then() and .catch() methods. They allow you to handle success and error cases in a more structured manner and enable sequential execution of asynchronous tasks.

    async/await: async/await is a modern syntax that provides a more synchronous-like way to write asynchronous code. It allows you to pause the execution of a function until a Promise resolves, making the code more readable and easier to understand.

    Callbacks: Callbacks are a traditional approach for handling asynchronous operations by passing a function to be called once the operation completes. They can be error-prone and lead to callback hell when dealing with multiple nested asynchronous operations.

    Event-based programming: Event-based programming is commonly used in environments like web browsers or Node.js, where you register event listeners to handle asynchronous events. It allows you to respond to events as they occur, making it suitable for scenarios with event-driven architectures.

These methods provide different ways to handle and manage asynchronous operations in JavaScript, each with its own benefits and use cases.

Also read JavaScript functions guide

Getting started with TypeScript guide

Don't Miss Out! Subscribe to Read Our Latest Blogs.

If you found this blog helpful, share it on social media.

Subscription form (#5)

Pin It on Pinterest

Scroll to Top