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.

Mark it as completed to track your progress, or bookmark it to review later.
Join senior engineers who receive practical, deep-dive frontend challenges, detailed concepts, and blueprints directly in their inbox.
We value your privacy. Unsubscribe at any time.
Expand your mastery. Deep dive into other frontend interview challenges in this category.
Understand the core JavaScript array methods. Learn the critical difference between mutating methods (push, pop, splice) and non-mutating methods (slice, concat, map).
Understand the key differences between ES6 arrow functions and traditional regular functions, particularly regarding the 'this' keyword, arguments object, and constructor usage.
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:
async/await interact with the Event Loop and Microtask Queue?async/await under the hood?Let's break down async/await from first principles with real-world analogies and advanced examples.
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.
.then() / .catch())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()).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.The async keyword is placed before a function declaration to turn it into an asynchronous function.
Important Interview Fact: Every
asyncfunction 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"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.
await block the main thread?No. This is a common trap question.
await blocks JavaScript until the operation is done."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");1. Global script - start
2. Inside async - start
3. Global script - end
4. Inside async - endIf 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.
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:
async function getUserData(userId) {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
return posts;
}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
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).
A common performance bottleneck in modern web apps is the Sequential Await Trap (waterfalling requests).
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 };
}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.allSettledPromise.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.While try/catch is the standard way to handle errors in async functions, there are key gotchas developers must avoid.
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);
}
}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
}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.
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
});await yields execution control back to the call stack and event loop. Resumed execution steps are scheduled as microtasks.Promise.all or Promise.allSettled to fetch parallel data.try/catch blocks or append a .catch() statement to awaited expressions to prevent unhandled promise rejections.