async/await — syntactic sugar over promises
TL;DR
async/await is a language feature that lets you write async code that reads like synchronous code. Under the hood it's still promises — you're just swapping .then() chains for a top-to-bottom flow that's easier to read and easier to debug.
The readability win
Before async/await, chained .then() calls were the norm. They worked, but once you had a handful of dependent async steps, the code turned into a hard-to-follow staircase.
Fetch a user, then fetch their orders:
Promise chain:
function getUserOrders(userId) {return fetch(`/api/users/${userId}`).then((response) => response.json()).then((user) => fetch(`/api/orders/${user.orderId}`)).then((response) => response.json()).then((order) => {console.log(order);});}
Same thing with async/await:
async function getUserOrders(userId) {const userResponse = await fetch(`/api/users/${userId}`);const user = await userResponse.json();const orderResponse = await fetch(`/api/orders/${user.orderId}`);const order = await orderResponse.json();console.log(order);}
The second reads top-to-bottom like ordinary code. Each await pauses the function until that promise resolves, then resumes with the result.
What async actually does
Adding async to a function does exactly two things:
- The function's return value is always wrapped in a promise. If you return a plain value, it gets
Promise.resolve'd for you. - You're allowed to use
awaitinside.
async function greet() {return 'Hello';}// This is the same asfunction greet() {return Promise.resolve('Hello');}greet().then((message) => console.log(message)); // "Hello"
What await actually does
await suspends the current async function until the awaited promise settles, then unwraps its resolved value.
async function loadData() {console.log('Start');const data = await fetch('/api/data'); // pauses hereconsole.log('Done'); // runs after fetch completes}
Key point people miss: await only pauses this function. The rest of the program keeps running — that's the whole point of it being async.
Error handling
With promises you chain .catch(). With async/await you use the same try...catch you'd use for sync code:
async function getUser(id) {try {const response = await fetch(`/api/users/${id}`);const user = await response.json();return user;} catch (error) {console.log('Something went wrong:', error);}}
Common Pitfalls
The biggest footgun is forgetting to handle errors at all. .then() chains made .catch() a visible part of the shape; with async/await it's easy to just not write a try, at which point a rejected promise becomes an unhandled rejection. Wrap any awaited call that can reasonably fail.
The two classic mistakes
Mistake 1: Serialising work that should run in parallel
// Slow — each request waits for the previous oneasync function getUsers(ids) {const users = [];for (const id of ids) {const user = await fetchUser(id);users.push(user);}return users;}// Fast — all requests run at the same timeasync function getUsers(ids) {const users = await Promise.all(ids.map((id) => fetchUser(id)));return users;}
Five users × 1s each = 5 seconds serially, ~1 second in parallel.
Mistake 2: Forgetting async functions always return promises
async function getData() {return 42;}// This does not workconst result = getData();console.log(result); // Promise { 42 }, not 42// You need to await it or use .then()const result = await getData(); // inside another async functionconsole.log(result); // 42
Interview Tip
Show the .then() version first, then the async/await rewrite. It demonstrates that you know what the sugar is sugaring. Finish by mentioning Promise.all — interviewers want to hear that you know when to parallelise.
Why interviewers ask this
Every frontend app talks to APIs. The question checks that you understand async flow, error handling, and the parallel-vs-sequential tradeoff. That's the difference between correct-but-slow code and correct-and-fast code.