Asynchronous Programming in JavaScript with Async & Await

Oguzhan Yuksel
ÇSTech
Published in
8 min readJun 9, 2023

JavaScript is a scripting language primarily based on the concept of single-threaded execution.

Source: https://www.freecodecamp.org/news/content/images/2021/09/freeCodeCamp-Cover-2.png

What is Single Thread execution?

If a long-running operation is performed, the browser or application cannot respond to the user until the operation is completed, resulting in a frozen or unresponsive appearance, which is not desirable in terms of user experience.

For example, let’s consider a scenario where you want to add a product to your shopping cart on an e-commerce website, and the addition to the cart is proceed through an API. Let’s assume that we send a request to the backend for each product added.

Earlier, we mentioned that the code is processed step by step in a sequential manner, and we cannot move on to the next task until the current one is completed. So, does that mean we have to wait without doing anything until the request is completed? And what if it takes 30 seconds or more to receive a response from this process? Will we just sit and wait for those 30 seconds? This is where the need for asynchronous programming comes into play.

Why do we need asynchronous operations, and what benefits do they provide?

Asynchronous operations, in its simplest definition, allow other operations to continue executing regardless of the progress or completion of a specific operation. This enables other tasks to run simultaneously while waiting for the result of a long-running operation, preventing the browser or application from freezing or becoming unresponsive.

In JavaScript, there are several ways to achieve this syntactically:

  • Callbacks
  • Promises
  • Async & Await

Callbacks

In asynchronous JavaScript functions, callback functions will be called when an operation is completed, instead of waiting for the completion of that operation. When an operation is asynchronous, the program flow is not completely blocked, and the secondary function continues its execution while the operation is being completed. Once the operation is finished, the specified callback function is executed.

function asyncFunction(callback) {
console.log("Operation started.");

setTimeout(function() {
console.log("Operation completed.");
callback();
}, 2000);
}

function callbackFunction() {
console.log("Callback function called.");
console.log("Operation finished, you can proceed.");
}

// Calling asyncFunction and passing callbackFunction as an argument
asyncFunction(callbackFunction);

console.log("Program continues...");

In this example, we have a function called asyncFunction. This function waits for 2 seconds and then completes an operation, followed by calling the specified callback function. The setTimeout function delays the operation by 2 seconds and then executes the callback function when the operation is completed.

The callbackFunction is the callback function in this example, which simply prints some messages to the console.

The output of the code will be like,

Operation started.
Program continues...
Operation completed.
Callback function called.
Operation finished, you can proceed.

Let’s approach the same scenario using Promises.

Promises

Promise objects are a JavaScript feature used to handle asynchronous operations more effectively. Promise objects provide us with two important keys which are resolve and reject.

When the operation is successfully completed, resolve is called, while in case of errors, reject is called.

function asyncFunction() {
console.log("Operation started.");

return new Promise(function(resolve, reject) {
setTimeout(function() {
console.log("Operation completed.");
resolve();
}, 2000);
});
}

function callbackFunction() {
console.log("Promise resolved successfully.");
console.log("Operation finished, you can proceed.");
}

// Calling asyncFunction and using the returned Promise with then() and catch() methods
asyncFunction()
.then(callbackFunction)
.catch(function(error) {
console.log("Promise rejected with error: " + error);
});

console.log("Program continues...");

In addition to the mentioned information above, the then method is used to specify the callback function that will be executed when a Promise is successfully resolved. The catch method, on the other hand, is used to specify the callback function that will be executed when a Promise is rejected with an error.

The output of the code will be like,

Operation started.
Program continues...
Operation completed.
Promise resolved successfully.
Operation finished, you can proceed.

“In this case, the program flow continues immediately after the asyncFunction call, and the message “Program continues…” is printed to the console. After 2 seconds, the operation is completed, and resolve is called. Consequently, the callbackFunction specified with the then method is executed, printing the messages “Promise resolved successfully.” and “Operation finished, you can proceed.” to the console.

However, the code appears to be a bit lengthy and complex, right? In JavaScript, there is always a cleaner way of writing code :)

Now, we come to the main topic of our discussion, which is the Async & Await structure.

Async & Await

In any case, Async & Await helps us with performing asynchronous operations, just like callbacks and promises. In terms of syntax structure, they are not significantly different from the functions we are familiar with. MDN — Async Function

async function fetchData() {
// Asynchronous code locates here
}

Note: Just like regular functions, async functions can also accept parameters in the same way.

As we know with Promises, the success or failure of an operation is determined by the resolve and reject keywords, and we need to continue the process using .then() if the operation is successful or .catch() if it fails.

And here comes the help of await.

The await keyword is used to pause the execution of a function until a Promise is resolved, and it can only be used inside an async function. MDN — Await

async function fetchPostsData() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await response.json();
return data;
} catch (err) {
console.error('Error of fetching posts:', err);
throw err;
}
}

API Source: https://jsonplaceholder.typicode.com/

In this example, we used async/await to fetch user data from an API, parse the response as JSON, and handle any potential errors that may occur during the process.

Additionally, to handle any error that may occur in the Promise result, we wrapped the entire operation inside a try/catch block. We will delve into this topic in more detail later.

Let’s examine why we prefer working with async/await instead of callbacks through a simple example.

Callback Hell vs Async/Await

Callback Hell

function getData(callback) {
fetchData(function (err, data) {
if (err) {
callback(err);
} else {
processData(data, function (err, result) {
if (err) {
callback(err);
} else {
callback(null, result);
}
});
}
});
}

Async/Await

async function getData() {
try {
const data = await fetchData();
const result = await processData(data);
return result;
} catch (err) {
throw err;
}
}

They both accomplish the same task. Which one do you prefer?

Error Handling with Try & Catch

Another benefit of using Async/Await is that it provides us with an easy and understandable structure for error handling. Additionally, with Try & Catch, we can handle errors in an asynchronous operation as if they were occurring in synchronous code.

If we consider our previous example in case of error inside the Catch block,

  • We can trigger error notifications,
  • Through currying, we can repeatedly invoke a function until it executes,
  • We can redirect the user to a different page and perform many other operations that I haven’t mentioned.

Triggering Error Toast Message code example

async function fetchPostsData() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const data = await response.json();
return data;
} catch (err) {
triggerErrorToastMessage({message: ‘Request failed’ });
throw err;
}
}

Let's see another Asynchronous usage for handling multi requests with Parallel and Sequential Execution

Parallel and Sequential Execution

Parallel Execution with Promise.all()

Promise.all() is a built-in method in JavaScript. It allows us to run multiple Promises in parallel and returns the results of all the Promises in a single response. MDN — Promise.all()

async function fetchAndProcessData() {
const [data1, data2, data3] = await Promise.all([
fetch('https://jsonplaceholder.typicode.com/posts'),
fetch('https://jsonplaceholder.typicode.com/users'),
fetch('https://jsonplaceholder.typicode.com/comments)
]);

// Process the fetched data
const processedData = await processFetchedData(data1, data2, data3);

return processedData;
}

In this example, requests are processed in parallel using Promise.all(), and the retrieved data is then sequentially processed using async/await.

Sequential Execution with Async/Await

By default, async/await executes asynchronous tasks sequentially, meaning the next task won’t start until the previous one is completed. This can be useful in scenarios where you need tasks to be executed in a specific order or dependent on the result of a previous task.

Let’s create a sample scenario:

For a transaction, we will request the registered user to enter a One-Time Password (OTP).

  1. Check if the user is a registered user in the system,
  2. Check if the user’s email address is valid based on the result of the first step,
  3. Send an OTP to the user’s email address based on the result of the second step,
  4. Return the result of the third step.

Then, if the process is successful, trigger the function that displays the OTP login screen.

Syntax Example

async function performTasksSequentially() {
// First task
const task1Result = await checIsUserRegisteredUser();

// Second task after completing first task
const task2Result = await checkIsMailAddressValid(task1Result);


// Third task after completing second task
const task3Result = await sendOTPToValidMail(task2Result);

// Return the final result
return task3Result;
}

It is indeed possible to use these two structures together, but I won’t go into the details as it would make the article too long.

Since we have talked about the principles of synchronous and asynchronous operation, we can now take a look at how the process works in the Event Loop.

Event Loop

First of all, let me clarify that the relationship of the Event Loop with Events in HTML is as much as the relationship between JavaScript and Java :) So there is no relationship.

JavaScript has a runtime model based on the Event Loop, which is responsible for executing code, handling events, and progressing queued sub-tasks in the stack.

The Event Loop continuously operates by managing two main components: the Call Stack and the Event Queue.

The Call Stack keeps track of the functions being executed at any given time, while the Event Queue holds the pending tasks or events to be processed.

Source: https://yapayakademi.com/wp-content/uploads/2021/04/setTimeout-gif.gif

Comparison of an Event Loop with Synchronous and Asynchronous Code

Here, I would like to provide you with two simple code examples to visualize the concept together and help us understand it better.

Synchronous

function foo(){
console.log("foo");
}

function bar(){
console.log("bar");
}

function hello(){
foo();
bar();
console.log ("hello");
}

hello();
Source: https://dogukanbatal.com/wp-content/uploads/2022/06/ezgif-4-b8c6bf47fb.gif

Asynchronous

console.log("hello");

function world(){
console.log("world")
}

setTimeout(function cbFunction(){
console.log("Callback Function")
}, 1000);

world();
Source: https://dogukanbatal.com/wp-content/uploads/2022/06/output_cQunjA.gif

To sum up, JavaScript, with its single-threaded execution model, manages asynchronous operations through its Event Loop, providing us with the ability to work asynchronously.

Don’t want to prolong it further as Event Loop is a topic that can be addressed separately, so I will conclude my article here.

Thank you for taking the time to read it! Your thoughts and comments are invaluable. Until we meet again, Arrivederci!

Additional Part

Here is my favorite video about Event Loop on Youtube: https://youtu.be/MJeofIcEWLo

You can reach me through the channels below,

LinkedIn: https://www.linkedin.com/in/developeroguzyuksel/

GitHub: https://github.com/ooguzyuksel

Oğuzhan YÜKSEL

Frontend Developer ÇSTech

References and Sources:

--

--