How is async/await different from Promises?
Advertisement
🧩 Scenario
Architecture Walkthrough
async/await Is Built on Promises
async/await is syntactic sugar introduced in ES2017. An async function always returns a Promise. When the function returns a value, that value is wrapped in a resolved Promise. When the function throws, the thrown value becomes a rejected Promise. The await keyword pauses execution within the async function until the awaited Promise settles, then unwraps the resolved value.
Underneath, the JavaScript runtime translates await expressions into the equivalent Promise .then() chains. No new asynchronous mechanism is introduced. This means that everything you know about Promises, including microtask queue timing, rejection handling, and chaining, still applies when using async/await.
Error Handling: try/catch vs .catch()
With Promises, errors are handled by appending .catch() to the chain or providing a rejection handler as the second argument to .then(). With async/await, the natural equivalent is a try/catch block around the await call. Both approaches handle rejected Promises, but they differ in readability and in how errors from multiple operations are grouped.
A single try/catch block can cover multiple await calls, which is concise but means you lose granularity: any of the awaited operations could be the one that threw. For production code, it is often better to wrap each critical await individually or use a helper that wraps each call in a tuple pattern ([error, data]) to maintain per-operation error context.
Sequential vs Parallel Execution
await pauses the current function until a Promise settles. If you await two Promises sequentially, the second does not start until the first finishes. For independent operations (two separate API calls that do not depend on each other), this is wasteful.
Promise.all() accepts an array of Promises and returns a single Promise that resolves when all of them settle. Using Promise.all([fetchUser(), fetchPosts()]) with a single await allows both requests to run in parallel. The total time is determined by the slowest operation rather than the sum of all operations.
Key Code Explained
// Promise chain version
function loadUser(id) {
return fetch(`/api/users/${id}`)
.then((res) => res.json())
.then((user) => {
console.log(user);
return user;
})
.catch((err) => console.error(err));
}
// Equivalent async/await version
async function loadUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
const user = await res.json();
console.log(user);
return user;
} catch (err) {
console.error(err);
}
}
// Sequential vs parallel
async function loadDashboard(userId) {
// Sequential: total time = time(user) + time(posts)
const user = await fetchUser(userId);
const posts = await fetchPosts(userId);
// Parallel: total time = max(time(user), time(posts))
const [user, posts] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
]);
}
// await inside forEach does NOT pause iteration
async function processItems(items) {
items.forEach(async (item) => {
await processItem(item); // does not pause the loop
});
// All items are "processing" simultaneously, forEach has already returned
// Correct: use for...of to preserve sequential awaiting
for (const item of items) {
await processItem(item); // pauses loop for each item
}
}
The forEach example is the most commonly misunderstood. The async callback passed to forEach returns a Promise, but forEach ignores that Promise and continues to the next iteration immediately. The outer function does not wait for any of the inner async operations to finish.
Tradeoffs
| Approach | Pro | Con |
|---|---|---|
| async/await | Readable, debuggable stack traces, natural try/catch | Can accidentally serialize parallel operations |
| Promise chains | Explicit data flow, composable | Verbose for multi-step sequences, harder to debug |
| Promise.all | Parallel execution, single await point | All Promises start regardless of earlier failures |
| Promise.allSettled | Runs all even if some fail, gives per-result status | More verbose to handle results, no short-circuit on failure |
What Interviewers Actually Check
- Whether you know async/await is syntactic sugar over Promises, not a separate mechanism
- Whether you can correctly convert a Promise chain to async/await and back
- Whether you know why await inside forEach does not work and what to use instead
- Whether you understand when to use Promise.all for parallel execution
- Whether you know an async function always returns a Promise even when it returns a primitive
Follow-Up Questions
- What is the difference between
Promise.allandPromise.allSettled, and when would you choose each? - If you
awaita value that is not a Promise (like a plain number), what happens? - How do you handle a situation where you want to run multiple async operations in parallel but cancel all of them if one fails?
- What is
Promise.raceand what use case does it solve? - A teammate proposes adding
asyncto every function by default "just in case." What are the downsides?
Common Candidate Mistakes
- Using
awaitinside aforEachloop and expecting each iteration to pause before the next starts - Not wrapping
awaitcalls intry/catchand losing visibility into rejected Promises - Awaiting three independent API calls sequentially when they could run in parallel with
Promise.all - Forgetting that an
asyncfunction always returns a Promise, so calling.then()on it is valid - Using top-level
awaitin a CommonJS file and getting a syntax error because it is only valid in ES modules
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you explain that async/await is syntactic sugar on top of Promises and does not introduce new runtime behavior?
- Can you convert a
.then()/.catch()chain into equivalent async/await code? - Can you explain why
awaitinsideforEachdoes not work and what to use instead? - Can you demonstrate running multiple async operations in parallel using
Promise.all? - Can you explain what an async function returns when it throws an error?
Summary
async/await is syntactic sugar over Promises. An async function always returns a Promise, and await pauses execution within the function until a Promise settles, producing cleaner sequential-looking code for operations that were previously written as .then() chains.
The main readability advantage is error handling: try/catch is more natural than chained .catch() calls for most developers, and stack traces in async/await code are easier to read in debuggers. The main pitfall is accidentally serializing independent operations by awaiting them one at a time. Any two operations that do not depend on each other's results should be run with Promise.all instead of sequential awaits.
Understanding the relationship between async/await and Promises is essential because the underlying Promise semantics still govern timing, error propagation, and cancellation behavior. async/await does not change how JavaScript handles asynchronous operations; it only changes how you write them.
Can I use await outside async functions?
No. await only works inside async functions (or top-level in ES2022+ modules).
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement