async/await — what it actually desugars to
async/await isn't a different way of doing asynchronous work than Promises — it's syntax that compiles down to exactly the same .then() chains, which is the fact that explains both why it reads top-to-bottom and where its real gotchas come from.
3 min read
async functions always return a Promise, even without an explicit one
async function getValue() {
return 42; // NOT a Promise in the function body...
}
getValue(); // ...but calling it returns Promise { 42 } — async ALWAYS wraps the return value
getValue().then((v) => console.log(v)); // 42The async keyword changes a function so that whatever it returns is automatically wrapped in a resolved Promise (or, if it throws, a rejected one) — even a plain return 42 becomes Promise<42> from the caller's perspective. This is worth internalizing explicitly: an async function's return value is never directly the value written after return — it's always a Promise that eventually resolves to that value.
await is sugar for .then() — literally, mechanically
// These two functions are functionally equivalent — await just reads top-to-bottom
async function withAwait() {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
return posts;
}
function withThen() {
return fetchUser().then((user) => fetchPosts(user.id));
}await somePromise pauses the async function's execution (without blocking the rest of the program — the call stack is freed up, and the pause resumes as a microtask once the Promise settles, exactly the mechanism the previous lesson covered) until the Promise resolves, then yields its resolved value directly, as if it were a synchronous expression. Every await in an async function is doing exactly what a .then() callback would do — the real, practical benefit is readability: sequential dependent steps read top-to-bottom like ordinary synchronous code, instead of nesting into .then() chains.
The real gotcha: sequential await creates an unintended waterfall
// SLOW — each await blocks the next call from even STARTING
async function loadDashboard() {
const user = await fetchUser(); // waits for this...
const posts = await fetchPosts(); // ...before this even starts, even though posts doesn't need user
const notifications = await fetchNotifications(); // ...same here
return { user, posts, notifications };
}
// FAST — all three start immediately, run concurrently
async function loadDashboard() {
const [user, posts, notifications] = await Promise.all([
fetchUser(), fetchPosts(), fetchNotifications(),
]);
return { user, posts, notifications };
}Because await's top-to-bottom readability makes async code look like ordinary sequential code, it's easy to write three independent await calls in a row without noticing they're now forced to run one after another, purely because of the order they're written in — even though none of them actually depends on a previous one's result. This is a real, common performance bug, and the fix (Promise.all, from the previous lesson) is ordinary Promise usage, not anything async/await-specific — the mistake is really a side effect of await's syntax making a waterfall easy to write by accident.
Error handling: try/catch instead of .catch()
async function loadUser(id) {
try {
const user = await fetchUser(id);
return user;
} catch (err) {
console.error("Failed to load user:", err);
return null;
}
}Because a rejected awaited Promise throws inside the async function (rather than returning a rejected Promise silently), ordinary try/catch — the same construct used for synchronous errors — works directly for async errors too, with no .catch() chain required. This is a genuine ergonomic win: async and sync error handling now share one familiar syntax, though it does mean forgetting the try/catch around an await results in an unhandled rejection propagating up exactly like an uncaught synchronous exception would.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What does an `async` function actually return, even when it has a plain `return 42;`?
2. What is `await` actually doing, mechanically?
3. Why can writing three independent `await` calls in a row create an unintended performance bug?