Understanding async/await in JavaScript
Asynchronous code is the part of JavaScript that trips up the most beginners — not because the concept is hard, but because the syntax used to get there changed twice. Here's why async/await won, and how to actually reason about it.
The callback problem
Early JavaScript handled anything asynchronous — reading a file, calling an API — with callback functions. Nesting a few of these together produces what's usually called "callback hell":
getUser(id, (user) => {
getPosts(user.id, (posts) => {
getComments(posts[0].id, (comments) => {
console.log(comments);
});
});
});
Each step only knows how to continue by calling the next function, and error handling has to be repeated at every level.
Promises flattened the nesting
A Promise represents a value that will exist eventually. Chaining .then() calls flattens the pyramid above:
getUser(id)
.then((user) => getPosts(user.id))
.then((posts) => getComments(posts[0].id))
.then((comments) => console.log(comments))
.catch((err) => console.error(err));
Better, but you're still writing in a style built around callbacks — just chained instead of nested.
async/await reads like synchronous code
await pauses execution inside an async function until a promise resolves, letting you write the same logic as if each step happened in order:
async function loadComments(id) {
try {
const user = await getUser(id);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
console.log(comments);
} catch (err) {
console.error(err);
}
}
Nothing about the underlying behavior changed — it's still promises under the hood, and await only works inside an async function. What changed is that a single try/catch handles errors from every step, and the logic reads top to bottom.
await only pauses the function it's inside — it doesn't make the rest of your program synchronous. Calling an async function without awaiting it will run the rest of your code before that function finishes.
When you still want Promise methods directly
If you need to run several independent async operations at once rather than one after another, reach for Promise.all instead of awaiting each in sequence:
const [user, settings] = await Promise.all([
getUser(id),
getSettings(id),
]);
Awaiting these one at a time would double the wait for no reason — they don't depend on each other.