JavaScript async/await: Deep Dive

Madhur GuptaMadhur Gupta
6 min read

JavaScript's asynchronous programming landscape has evolved significantly over the years. While Promises were a big step forward from callback hell, the introduction of async/await syntax has revolutionized how we write asynchronous code. In this article, we'll explore the limitations of using Promises with .then() and .catch() methods, discuss behind the scenes workings of async/await, and highlight the advantages that async and await keywords provide.

But before jumping into this deep dive, you should know about callbacks and promises. You can also check out these blogs on JavaScript Callbacks and Promises.

Limitations of handling promises using .then() and .catch()

  • Verbosity: Chaining multiple .then() calls can lead to code that's hard to read and maintain, especially for complex asynchronous operations.

  • Error Handling: With .then() and .catch(), error handling can become tricky, especially when you need to catch errors from specific parts of the chain.

  • Scoping Issues: Variables declared in one .then() block are not easily accessible in subsequent blocks without creating closures or using higher-scoped variables.

  • Complex Code Structure: Using promise chaining of multiple promises can lead to complex code structure, which can be difficult to handle and debug.

Q. What is async?

Async is a keyword that is used before a function to create an async function.

async function greet() {
  return "Hello World";
}

Q. What are async functions and how they are different from normal function?

Async functions are special types of functions which always returns a promise, even if we return a simple string from it, async keyword will wrap it under Promise and then return.

async function greet() {
  return "Hello World";
}
const dataPromise = greet();
console.log(dataPromise); // Promise {<fulfilled>: 'Hello World'}

To extract data from above promise, we can use promise .then().

dataPromise.then(res => console.log(res)); // Hello World

Q. How can we use await along with async function?

  • async and await combo is used to handle promises.
πŸ’‘
await is a keyword that can only be used inside an async function.
const p = new Promise((resolve, reject) => {
  resolve('Promise resolved value!!');
}); // Creating a new promise

async function handlePromise() {
  const val = await p;  // Waiting for promise to be resolved or rejected
  console.log(val);
}
handlePromise(); // Promise resolved value!!

Q. What makes async-await special?

Let's understand with an example where we will compare async-await way of resolving promise with older .then/.catch fashion. For that we will modify our promise p.

const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise resolved value!!');
  }, 3000);
});

πŸ“Œ Promise.then/.catch way:

async function handlePromise() {
  // JS Engine will wait for promise to resolve.
  const val = await p;
  console.log('Hello There!');
  console.log(val);
}
handlePromise(); // This time `Hello There!` won't be printed immediately instead after 3 secs `Hello There!` will be printed followed by 'Promise resolved value!!'
// πŸ’‘ So basically code was waiting at `await` line to get the promise resolve before moving on to next line.

πŸ“Œ async-await way:

async function handlePromise() {
  console.log('Hi');
  const val = await p;
  console.log('Hello There!');
  console.log(val);

  const val2 = await p;
  console.log('Hello There! 2');
  console.log(val2);
}
handlePromise(); 
// `Hi` printed instantly -> now code will wait for 3 secs -> After 3 secs both promises will be resolved so ('Hello There!' 'Promise resolved value!!' 'Hello There! 2' 'Promise resolved value!!') will get printed immediately.

Q. Does the program actually waits for the promise to be resolved?

The answer is NO. Over here it appears that JS engine is waiting but it’s not. It has not occupied the call stack. If that would have been the case our page may have got frozen. If it is not waiting, then what it is doing behind the scenes? Let's understand with below code snippet.

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise resolved value by p1!!');
  }, 5000);
})

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise resolved value by p2!!');
  }, 10000);
})

async function handlePromise() {
  console.log('Hi');
  const val = await p;
  console.log('Hello There!');
  console.log(val);

  const val2 = await p2;
  console.log('Hello There! 2');
  console.log(val2);
}
handlePromise();

When the function handlePromise() is executed, it will go line by line as JS is synchronous single threaded language. Let’s observe what is happening under call-stack.

Call stack flow β†’ handlePromise() is pushed β†’ It will log Hi to console β†’ Next it sees await keyword, where promise is supposed to be resolved β†’ So will it wait for promise to resolve and block call stack? No β†’ Thus handlePromise() execution gets suspended and moved out of call stack β†’ So when JS sees await keyword it suspends the execution of function till promise is resolved β†’ Promise p1 gets resolved after 5 seconds β†’ handlePromise() will be pushed to call-stack again after 5 secs. β†’ But this time it will start executing from where it had left. β†’ Now it will log 'Hello There!' and 'Promise resolved value!!' β†’ Then it will check whether p2 is resolved or not β†’ It will find that p2 will take 10 secs to resolve, so the same above process will repeat β†’ execution will be suspended until promise is resolved.

πŸ’‘
πŸ“Œ Thus JS engine is not waiting; call stack is not getting blocked.

Moreover, in above scenario what if p1 would be taking 10 secs and p2 5 secs β†’ even though p2 got resolved earlier but JS is synchronous single threaded language, so it will first wait for p1 to be resolved and then will immediately execute all.

Real World example of async/await

async function handlePromise() {
  // fetch() => Response Object which returns a Readable stream. 
  // Response.json() is also a promise which needs to be resolved.
  const data = await fetch('https://api.github.com/users/alok722');
  const res = await data.json();
  console.log(res);
};
handlePromise()

Error Handling in async/await

While we were using normal Promise we were using .catch to handle error, now in async-await we would be using try-catch block to handle error.

async function handlePromise() {
  try {
    const data = await fetch('https://api.github.com/users/abcd');
    const res = await data.json();
    console.log(res);
  } catch (err) {
    console.log(err)
  }
};
handlePromise()

// In above whenever any error will occur the execution will move to catch block.

Q. What you should use?

Async-await is just a syntactic sugar around promise. Behind the scenes async-await is just promise. So, both are same, it's just async-await is new way of writing code. It solves few of the shortcomings of Promises like Promise Chaining. It also increases the readability. So, it is always advisable to use async-await.

Summary

JavaScript's evolution from callback hell to Promises and now async/await has drastically improved how we handle asynchronous programming. This article explores the limitations of using Promises with .then() and .catch(), details the mechanics and advantages of async/await, and demonstrates its readability and error-handling improvements with code examples. Async/await, being syntactic sugar around Promises, not only simplifies code but also enhances maintainability.

2
Subscribe to my newsletter

Read articles from Madhur Gupta directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Madhur Gupta
Madhur Gupta