async/await
Syntactic Sugar That Reads Like Synchronous Code
LinkedIn Hook
async/awaitis the feature that made asynchronous JavaScript finally readable. But the exact reason it feels so natural is also the reason it hides some of the nastiest performance bugs in modern codebases.
awaitin aforloop is the most famous example — it looks clean, it reads like synchronous code, and it quietly turns three 500ms parallel requests into a 1500ms sequential chain. Multiply that across a real product and you've just burned seconds of user-perceived latency for no reason.In this lesson you'll see the full evolution — callbacks to Promises to
async/await— side by side, learn exactly what theasynckeyword guarantees about your return value, understand why forgettingawaitproduces a bug that's truthy and silent, and spot the classic "await in a loop" anti-pattern before it ships to production.If you write
awaitevery day but can't explain what an async function actually returns — this lesson fixes that.Read the full lesson -> [link]
#JavaScript #InterviewPrep #AsyncAwait #AsyncJS #Frontend #CodingInterview #WebDevelopment
What You'll Learn
- What
async/awaitactually is (spoiler: syntactic sugar over Promises) and what anasyncfunction returns - How to rewrite Promise chains with
try/catchfor linear, readable error handling - The "await in a loop" anti-pattern and when sequential vs parallel is the right choice
What Is async/await?
If Promises are the blueprint, async/await is the beautiful house built on top of it. It's syntactic sugar over Promises — same engine underneath, but code that reads like synchronous logic.
Imagine reading a recipe: "First get the flour, then add water, then knead the dough." That's how async/await reads. Without it (using .then() chains), the recipe would read more like: "Get the flour, and when you have it, add water, and when that's done, knead the dough."
The Full Evolution: Callback -> Promise -> async/await
// ============ CALLBACK STYLE ============
function getUserCallback(id, callback) {
setTimeout(() => callback(null, { id, name: "Rakibul" }), 500);
}
function getOrdersCallback(userId, callback) {
setTimeout(() => callback(null, [{ id: 101, item: "Laptop" }]), 500);
}
getUserCallback(1, (err, user) => {
if (err) return console.error(err);
getOrdersCallback(user.id, (err, orders) => {
if (err) return console.error(err);
console.log(orders); // nested, messy
});
});
// ============ PROMISE STYLE ============
function getUserPromise(id) {
return new Promise((resolve) => {
setTimeout(() => resolve({ id, name: "Rakibul" }), 500);
});
}
function getOrdersPromise(userId) {
return new Promise((resolve) => {
setTimeout(() => resolve([{ id: 101, item: "Laptop" }]), 500);
});
}
getUserPromise(1)
.then((user) => getOrdersPromise(user.id))
.then((orders) => console.log(orders)) // flat, better
.catch((err) => console.error(err));
// ============ ASYNC/AWAIT STYLE ============
async function fetchUserOrders() {
try {
const user = await getUserPromise(1);
const orders = await getOrdersPromise(user.id);
console.log(orders); // reads like sync code!
} catch (err) {
console.error(err);
}
}
fetchUserOrders();
Error Handling with try/catch
async function loadDashboard() {
try {
const user = await fetchUser();
const profile = await fetchProfile(user.id);
const notifications = await fetchNotifications(user.id);
renderDashboard({ user, profile, notifications });
} catch (error) {
// Catches any rejection from any of the awaits above
console.error("Dashboard load failed:", error.message);
showErrorUI();
} finally {
hideLoadingSpinner();
}
}
One try/catch is enough for the whole block — any rejected await jumps control straight into the catch, just like a synchronous throw.
The "await in a Loop" Antipattern
This is one of the most common performance mistakes — and a frequent interview question:
// BAD: Sequential — each request waits for the previous one
// Total time: 500 + 500 + 500 = 1500ms
async function getSequential(userIds) {
const users = [];
for (const id of userIds) {
const user = await fetchUser(id); // waits each time!
users.push(user);
}
return users;
}
// GOOD: Parallel — all requests fire at once
// Total time: ~500ms (slowest single request)
async function getParallel(userIds) {
const promises = userIds.map((id) => fetchUser(id)); // fire all at once
const users = await Promise.all(promises); // wait for all
return users;
}
When to use sequential: When each request depends on the result of the previous one (e.g., paginated APIs where page 2's URL comes from page 1's response).
When to use parallel: When requests are independent (e.g., fetching data for multiple users).
What async Functions Actually Return
An async function always returns a Promise:
async function add(a, b) {
return a + b; // automatically wrapped in Promise.resolve()
}
add(2, 3).then((result) => console.log(result)); // 5
// Even throwing an error returns a rejected Promise
async function fail() {
throw new Error("boom");
}
fail().catch((err) => console.error(err.message)); // "boom"
Common Mistakes
- Forgetting
awaitbefore a Promise — you get the Promise object (which is truthy) instead of its resolved value, producing silent bugs likeif (fetchUser())always being true. - Using
awaitinside aforloop for independent requests — you turn what should be parallel work into a sequential chain and inflate latency linearly. - Assuming a
try/catchinside a non-async function can catch an awaited rejection —awaitonly exists insideasyncfunctions (or top-level in ES modules); using it elsewhere is a syntax error.
Interview Questions
Q: What does the async keyword do?
It marks a function as asynchronous and guarantees it returns a Promise. If the function returns a non-Promise value, it's automatically wrapped in
Promise.resolve(). If the function throws, it returns a rejected Promise.
Q: What happens if you forget to use await before a Promise?
You get the Promise object itself, not its resolved value. The code continues without waiting for the async operation. This is a common bug —
if (fetchUser())is always truthy because a pending Promise is a truthy object.
Q: Why is await inside a for loop often a performance problem?
Each iteration waits for the previous
awaitto complete before starting the next one, making requests sequential. If the operations are independent, usePromise.all()to run them in parallel.
Q: Can you use await at the top level?
Yes, in ES modules (type="module" in browsers, or .mjs files in Node.js). It's called "top-level await." In CommonJS (require-based) modules, you need to wrap it in an async IIFE.
Q: What does the async keyword guarantee about a function's return value?
The function always returns a Promise, regardless of what's inside — a returned value is wrapped in
Promise.resolve(value), a thrown error producesPromise.reject(error), and a returned Promise is passed through (the outer async function's Promise settles when the inner one does).
Q: What happens if you await a non-Promise value?
It's wrapped in
Promise.resolve(value)on the spot, the current function pauses for one microtask, then resumes with that value. Soawait 42is valid and yields42— just with a microtask delay.
Quick Reference — Cheat Sheet
ASYNC / AWAIT — QUICK MAP
Shape:
async function fn() {
try {
const result = await somePromise();
} catch (err) {
// handle rejection
} finally {
// cleanup
}
}
Guarantees:
- async fn always returns a Promise
- return value -> Promise.resolve(value)
- thrown error -> Promise.reject(error)
- await unwraps a Promise; await 42 works too
Sequential vs Parallel:
// Sequential (slow, 3 * 500ms)
for (const id of ids) await fetchUser(id);
// Parallel (fast, ~500ms)
await Promise.all(ids.map(id => fetchUser(id)));
Top-level await:
Allowed in ES modules; wrap in async IIFE in CommonJS.
Previous: Promises — Chaining & Error Handling Next: Promise Combinators — all, race, allSettled, any
This is Lesson 3.8 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.