*
Previous Destructuring Optional chaining Next

πŸ”„ Deep Dive: Async/Await in JavaScript

In JavaScript, async/await is a modern, clean, and highly readable syntax for writing asynchronous code, introduced in ECMAScript 2017. It is syntactic sugar built on top of Promises, simplifying their use by making asynchronous operations appear more like synchronous code.

πŸ“˜ What is Async/Await?

async and await are syntactic sugar built on top of Promises, introduced in ES2017. They make asynchronous code look and behave more like synchronous code, improving readability and maintainability.

πŸ§ͺ Basic Syntax

// Define an async function
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error:', error);
  }
}
fetchData();

βš™οΈ How It Works

  • async functions always return a Promise.
  • await pauses the execution until the Promise resolves or rejects.
  • It allows writing asynchronous code in a synchronous style.

The basics of async and await

The async keyword

You must declare a function with the async keyword to use the await keyword inside it.

An async function always returns a Promise. Any value you explicitly return from an async function is automatically wrapped in a resolved Promise.

If an exception is thrown inside an async function, the returned Promise is automatically rejected with that error.

The await keyword

The await keyword is used inside an async function to pause its execution until the Promise it is waiting for is settled (resolved or rejected).

It then returns the resolved value of the Promise. In case of a rejection, await throws an exception, which can be caught with a try...catch block.

Crucially, await does not block the entire main thread of JavaScript. It only pauses the execution of the async function itself, allowing the rest of the program to continue.

How it works under the hood

When the JavaScript engine encounters async/await, it transforms the code into a state machine at compile-time.

Encountering await: When an await is hit, the engine pauses the execution of the async function and puts the rest of the function in a queue (the Microtask Queue) to be run later.

Unblocking the main thread: The engine is now free to execute other synchronous code.

Resuming execution: When the awaited Promise is settled, the event loop picks the paused function from the Microtask Queue and pushes it back onto the Call Stack to continue its execution right where it left off.

Benefits of async/await

  • Readability: It allows asynchronous code to be written and read in a synchronous, top-to-bottom manner, making it much easier to follow than complex .then() promise chains.
  • Simpler error handling: It allows for the use of standard try...catch blocks for error handling, which is a familiar pattern for synchronous code and often cleaner than using a .catch() block.
  • Easier debugging: The sequential, synchronous-like flow of async/await makes it easier to debug, as the code flow is more predictable.
  • Reduced "Callback Hell": Just like Promises, async/await solves the problem of deeply nested callback functions, also known as "callback hell".

Drawbacks and precautions

  • Accidental blocking: Careless use of await inside a synchronous loop can cause a series of asynchronous calls to run in sequence, even if they could have run in parallel, hurting performance.
  • Overhead: While usually negligible, async/await introduces a small amount of overhead compared to using Promises directly.
  • Root async calls: You cannot use await at the top level of a script in older environments. A workaround is to wrap it in an immediately invoked async function expression (IIFE).
  • Requires Promises: async/await is not a replacement for Promises but a simpler syntax on top of them. You must use it with functions that already return Promises, such as fetch().

Best practices and use cases

Running tasks in parallel

To avoid awaiting sequential, independent promises, run them concurrently using Promise.all() and then await the result.

//javascript
async function fetchMultipleUrls(urls) {
  try {
    const promises = urls.map(url => fetch(url));
    const responses = await Promise.all(promises);
    const data = await Promise.all(responses.map(res => res.json()));
    return data;
  } catch (error) {
    console.error("Error fetching data:", error);
  }
}

  

Error handling

Always wrap your await calls in a try...catch block to handle potential promise rejections gracefully.

//javascript
async function getUser(id) {
  try {
    const response = await fetch(`https://api.example.com/users/${id}`);
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    console.error("Failed to fetch user:", error);
    // You can re-throw or return a default value
    return null;
  }
}

  

Top-level await

If you are working in an ES module environment (e.g., modern browsers or Node.js with "type": "module"), you can use await at the top level without an async function.

//javascript
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
  

βœ… Advantages

  • Improved readability and cleaner syntax
  • Easier to debug with try/catch blocks
  • Reduces callback nesting (callback hell)
  • Works seamlessly with Promises

❌ Disadvantages

  • Only usable inside async functions
  • Does not run in parallel unless explicitly handled (e.g., Promise.all)
  • Can lead to performance bottlenecks if used improperly

πŸ“Œ When to Use

  • When you need to perform sequential asynchronous operations
  • When you want to simplify Promise chains
  • When error handling is important (use try/catch)

🚫 When Not to Use

  • When you need to run tasks in parallel (use Promise.all instead)
  • In performance-critical loops without batching
  • In top-level code (unless using top-level await in modern environments)

🧠 Best Practices

  • Use try/catch for error handling
  • Use Promise.all for parallel execution
  • Don’t block on independent async calls
  • Use descriptive function names for clarity

πŸ’‘ Tips

  • Use async IIFEs for top-level await:
    (async () => {
      const result = await fetchData();
    })();
  • Combine with map() and Promise.all() for batch processing:
    const urls = ['url1', 'url2'];
    const results = await Promise.all(urls.map(url => fetch(url)));

πŸ”— Further Reading

Back to Index
Previous Destructuring Optional chaining Next
*
*