Async/Await vs Promises: Cleaning Up Deeply Nested Code
3 min read
Every JavaScript developer eventually writes the pyramid: a .then() inside a .then()
inside a .then(), with error handling bolted somewhere it doesn't quite cover. Then
someone shows them async/await and the pyramid flattens into something readable. This
post is that transformation — plus the two things people consistently get wrong after
making it.
The 3-level-deep problem
A realistic sequence: fetch a user, then their active project, then that project's tasks:
function loadDashboard(userId) {
return fetchUser(userId).then((user) => {
return fetchProject(user.activeProjectId).then((project) => {
return fetchTasks(project.id).then((tasks) => {
return { user, project, tasks };
});
});
});
}Notice why it nests: each step needs a variable from an earlier step (user at the
end), so you can't flatten the chain without losing access to it. The indentation is
annoying; the scoping problem is the real disease.
What async/await actually is
Not a replacement for Promises — syntax over Promises. An async function always
returns a Promise; await pauses the function (without blocking the thread — the
event loop carries on) until a Promise
settles, then hands you its value as a plain variable. Same machinery, sequential
syntax:
async function loadDashboard(userId) {
const user = await fetchUser(userId);
const project = await fetchProject(user.activeProjectId);
const tasks = await fetchTasks(project.id);
return { user, project, tasks };
}The scoping problem dissolves: every intermediate value is a normal const, available
for the rest of the function. Error handling becomes ordinary try/catch covering the
whole sequence. Debugging improves too — step-through debuggers and stack traces treat
this like the sequential code it reads as.
Because it's still Promises underneath, the two styles interoperate freely: you can
await any Promise-returning library, and callers can .then() your async function.
Migration can be incremental.
Where you still need Promise.all
Here's the subtle downgrade hiding in await: it's sequential. If steps don't depend
on each other, awaiting them one by one serializes work that could run in parallel:
// ❌ Sequential: ~600ms if each takes ~200ms
const profile = await fetchProfile(id);
const notifications = await fetchNotifications(id);
const settings = await fetchSettings(id);
// ✅ Parallel: ~200ms — start all three, then wait once
const [profile, notifications, settings] = await Promise.all([
fetchProfile(id),
fetchNotifications(id),
fetchSettings(id),
]);The rule: await expresses dependency; Promise.all expresses independence. The
dashboard example genuinely is dependent (each call needs the previous result), so
sequential is correct there. Three unrelated fetches are not. Also worth knowing:
Promise.allSettled when you want all results even if some fail, and Promise.race
for timeouts.
Common async/await mistakes
- Forgetting errors entirely. A rejected Promise in an
asyncfunction withouttry/catch(or a.catch()upstream) becomes an unhandled rejection. Wrap the await sequence, and decide per call whether a failure is fatal or recoverable. - Accidental serialization. The
Promise.allcase above — the most common performance bug I find in code review, invisible because the code works, just slower than it should be. awaitin loops.for (const id of ids) await fetchItem(id)fires requests one at a time. Often you wantawait Promise.all(ids.map(fetchItem))— but mind rate limits; sometimes sequential is the polite choice. Make it a decision, not an accident.- Mixing styles half-heartedly.
await fn().then(...)works but reads like a sentence that switches language halfway. Pick the async/await register and stay in it.
The takeaway
Use async/await as your default register — it reads sequentially, scopes naturally, and
error-handles conventionally. Keep Promises in your active vocabulary for what they
express better: parallelism (Promise.all), aggregation (allSettled), and racing
(race). The developers who struggle here are the ones who learned the syntax as a
replacement; the ones who thrive learned it as a view over the same Promise machinery
— because then every "weird" behavior has an explanation.