New: Master Frontend System Design! Read our brand new Meta-Guide and solve the Autocomplete practice challenge.Read Master Guide
FrontendPrep
javascriptMedium

JavaScript Async/Await Interview Questions

Loading...

Master JavaScript async/await with practical examples and real-world explanations. Learn how async functions, await, promises, error handling, concurrency, and the event loop work under the hood.

Arvind M
Arvind MLinkedIn

JavaScript Async/Await Interview Questions

One of the most frequently asked JavaScript interview questions is:

What is async/await, how does it work under the hood, and does it block the execution thread?

While many developers can write basic async functions, interviewers often look for a deeper understanding:

  • How does async/await interact with the Event Loop and Microtask Queue?
  • Does it block the main thread?
  • What are the performance implications of sequential vs. parallel execution?
  • How does JavaScript compile async/await under the hood?

Let's break down async/await from first principles with real-world analogies and advanced examples.


1. The Coffee Shop Analogy: Understanding Asynchronous execution

To fully understand why JavaScript evolved from callbacks to Promises and eventually to async/await, let's imagine a coffee shop where customers order lattes.

Synchronous (Blocking)

  • The Scene: You walk up to the cashier and order a Latte. The cashier stops taking orders, walks to the back to brew the coffee, pours it, hands it to you, and only then returns to take the next order.
  • The Problem: The entire line of 20 people behind you is frozen, unable to place orders.
  • JavaScript Equivalence: Synchronous execution blocks the single main thread. While waiting for a network request, the browser's UI completely freezes and becomes unresponsive to clicks or scrolls.

Callbacks (Callback Hell)

  • The Scene: You order your Latte. The cashier registers your order and hands you a buzzer. The cashier instructs the barista: "Once the coffee is done, beep the customer, tell them to get their cup, have them sign the receipt, and report back to the manager."
  • The Problem: While you can walk away and wait, writing complex workflows leads to highly nested, unreadable code.
  • JavaScript Equivalence: Callback functions nested inside callback functions, commonly known as the "Pyramid of Doom" or "Callback Hell".

Promises (.then() / .catch())

  • The Scene: You order a Latte. The cashier hands you a buzzer (representing a Promise). You walk away to sit at a table, browse your phone, and relax. When the buzzer flashes (resolved), you walk back to fetch your coffee (.then()). If the machine breaks down, the buzzer flashes red (rejected), and you go to request a refund (.catch()).
  • JavaScript Equivalence: Promises provide a clean, chained structure to handle asynchronous actions without deep nesting.

Async/Await

  • The Scene: You order a Latte. The syntax makes it look like you are standing directly at the register waiting: const coffee = await orderCoffee(). However, under the hood, JavaScript registers your buzzer, pauses the execution of your ordering workflow, and immediately starts serving the next customer. When the coffee is ready, JavaScript resumes your order right where you left it.
  • JavaScript Equivalence: An asynchronous function that reads like synchronous code, offering the readability of synchronous execution with the performance benefits of Promises.

2. What is an Async Function?

The async keyword is placed before a function declaration to turn it into an asynchronous function.

Important Interview Fact: Every async function always returns a Promise.

If you return a non-promise value inside an async function, JavaScript automatically wraps it in a resolved Promise.

async function greet() {
  return "Hello, World!";
}
 
console.log(greet()); // Output: Promise { <resolved>: "Hello, World!" }
 
// Equivalent to:
function greetEquivalent() {
  return Promise.resolve("Hello, World!");
}

If an error is thrown inside an async function, it returns a rejected Promise:

async function fail() {
  throw new Error("Something went wrong");
}
 
fail().catch(err => console.log(err.message)); // Output: "Something went wrong"

3. What is Await and Does it Block JavaScript?

The await keyword can only be used inside an async function (with the exception of Top-Level Await in ES modules). It pauses the execution of the async function until the awaited Promise resolves or rejects.

Does await block the main thread?

No. This is a common trap question.

  • Incorrect Answer: "Yes, await blocks JavaScript until the operation is done."
  • Correct Answer: "await suspends execution of the current async function, but it does not block the main JavaScript execution thread. It yields control back to the Event Loop, allowing other tasks, rendering processes, and event handlers to continue running."

Let's prove this with a code example:

async function test() {
  console.log("2. Inside async - start");
  await Promise.resolve(); // Suspends this function, control returns to main script
  console.log("4. Inside async - end");
}
 
console.log("1. Global script - start");
test();
console.log("3. Global script - end");

Execution Flow Output:

1. Global script - start
2. Inside async - start
3. Global script - end
4. Inside async - end

If await blocked the thread, the output would be 1 ➔ 2 ➔ 4 ➔ 3. Instead, the execution context of test() was suspended at the await expression, allowing the global script to finish printing 3 before resuming the async function to log 4.


4. Under the Hood: Generators and Iterators

Before native async/await was introduced in ES2017, developers used Generators (function* and yield) combined with helper libraries (like co) to write synchronous-looking asynchronous code.

Compilers like Babel transform async/await code into generator functions under the hood.

Here is how JavaScript conceptually translates async/await into generators:

Written Code:

async function getUserData(userId) {
  const user = await fetchUser(userId);
  const posts = await fetchPosts(user.id);
  return posts;
}

Compiled/Transformed Representation:

function getUserData(userId) {
  // A generator function handles yielding steps
  return spawn(function* () {
    const user = yield fetchUser(userId);
    const posts = yield fetchPosts(user.id);
    return posts;
  });
}
 
// Spawn helper that drives the generator automatically
function spawn(generatorFunc) {
  return new Promise((resolve, reject) => {
    const generator = generatorFunc();
    
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch (e) {
        return reject(e);
      }
      if (next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(
        val => step(() => generator.next(val)),
        err => step(() => generator.throw(err))
      );
    }
    
    step(() => generator.next());
  });
}

await acts as a yield point, passing control back to the executor (spawn), which waits for the promise to resolve before passing the value back into the generator with .next(val).


5. Performance Optimization: Sequential vs. Parallel Awaits

A common performance bottleneck in modern web apps is the Sequential Await Trap (waterfalling requests).

The Bottleneck: Sequential Execution

If you have two independent asynchronous operations, awaiting them sequentially forces them to run one after another, doubling the loading time.

// ❌ Takes ~2 seconds to run
async function getProfileData() {
  const user = await fetchUser();   // Takes 1s
  const posts = await fetchPosts(); // Takes 1s (unnecessarily delayed)
  return { user, posts };
}

The Solution: Parallel/Concurrent Execution

If the calls are independent, start both requests concurrently and wait for both to complete together.

// ✅ Takes ~1 second to run
async function getProfileDataParallel() {
  // Start both promises concurrently in the background
  const userPromise = fetchUser();
  const postsPromise = fetchPosts();
  
  // Await them simultaneously
  const [user, posts] = await Promise.all([userPromise, postsPromise]);
  return { user, posts };
}

Promise.all vs Promise.allSettled

  • Promise.all: Rejects immediately if any of the promises reject. Best when you need all operations to succeed to proceed.
  • Promise.allSettled: Waits for all promises to settle (either resolve or reject) and returns an array of descriptors. Best when you want to handle partial successes independently.

6. Error Handling Gotchas

While try/catch is the standard way to handle errors in async functions, there are key gotchas developers must avoid.

Gotcha 1: The Forgotten Await in Try/Catch

If you return a promise from a try/catch block without awaiting it, the catch block will not intercept any rejections.

// ❌ Catch block is bypassed
async function loadData() {
  try {
    return fetchUserData(); // Error thrown here will escape the catch block!
  } catch (error) {
    console.log("Caught:", error.message);
  }
}

Why? The function returns the Promise immediately and pops off the stack. When the Promise eventually rejects, the try/catch context has already been destroyed.

Correction: Await the promise before returning it so the rejection is caught inside the function context:

// ✅ Correct Error Interception
async function loadData() {
  try {
    const data = await fetchUserData();
    return data;
  } catch (error) {
    console.log("Caught:", error.message);
  }
}

7. Advanced Interview Q&A

Q1: Can await be used with non-promise values?

Yes. If you await a non-promise value, JavaScript automatically converts it to a resolved promise.

async function test() {
  const value = await 42; // Equivalent to: await Promise.resolve(42)
  console.log(value); // 42
}

Q2: What happens if you forget the async keyword but use await?

Unless you are inside an ES Module using top-level await, it throws a SyntaxError: await is only valid in async functions.

Q3: How do you handle errors when you want to bypass try/catch wrappers?

You can catch errors directly on the awaited Promise, allowing you to return default values cleanly:

const user = await fetchUser().catch(err => {
  console.error(err);
  return { defaultUser: true }; // Fallback value
});

Key Takeaways for Interviews

  1. Syntactic Sugar: Async/await is built directly on top of Promises and Generators. It improves readability without altering JavaScript's asynchronous model.
  2. Event Loop integration: await yields execution control back to the call stack and event loop. Resumed execution steps are scheduled as microtasks.
  3. Concurrency is key: Do not chain awaits sequentially unless one depends on the result of the other. Use Promise.all or Promise.allSettled to fetch parallel data.
  4. Always Catch: Use try/catch blocks or append a .catch() statement to awaited expressions to prevent unhandled promise rejections.

Finished practicing this challenge?

Mark it as completed to track your progress, or bookmark it to review later.

Loading...

Share this Resource

Help other developers level up by sharing this study guide.

⚡ Weekly newsletter

Crack Your Next Frontend Interview.

Join senior engineers who receive practical, deep-dive frontend challenges, detailed concepts, and blueprints directly in their inbox.

  • Senior level React, JS, and CSS interview blueprints
  • System Design & performance optimization deep-dives
  • 100% free, zero spam, unsubscribe with one click

Join the Study Track

We value your privacy. Unsubscribe at any time.

More Technical Questions

Expand your mastery. Deep dive into other frontend interview challenges in this category.