I see this mistake in code reviews all the time. The code looks clean, the indentation is calm, and every line has an await in the right place. Then you open the Network panel and realize the page is waiting three times longer than it needs to.
The problem is not async/await. The problem is using it too literally. If three async operations do not depend on each other, waiting for them one by one is just a slow waterfall with nicer syntax.
The Mistake
This is the pattern that quietly makes pages feel slow:
// Slow: ~3 seconds total
async function loadAllSlow() {
const a = await fetchA(); // wait 1s
const b = await fetchB(); // wait 1s
const c = await fetchC(); // wait 1s
return [a, b, c];
}Nothing is technically broken. The result is correct. But the timing is wrong. fetchB() does not start until fetchA() finishes, and fetchC() does not start until fetchB() finishes.
If those requests are independent, this is the version you usually want:
// Fast: ~1 second total
async function loadAllFast() {
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
return [a, b, c];
}The difference is simple: the slow version starts one job at a time. The fast version starts all jobs immediately, then waits for the group.
Promise.all()fulfills when all input promises fulfill, and rejects when any input promise rejects. MDN Web Docs
The Question I Ask Before Every await
Before I write the next await, I ask one question: does this line need the result from the previous line?
- If yes, keep it sequential.
- If no, start the work together with
Promise.all().
That one question catches most performance bugs in async code. It also keeps the code honest. Sequential code is not bad. Accidental sequential code is.
fetchOrders(user.id) needs the user returned from fetchUser(), it must wait. Use parallel execution only when the operations are truly independent.Live Runner: Same Work, Different Timing
Run the demo below. Both columns do the same three fake requests: 600ms, 700ms, and 800ms. The only difference is whether they start one after another or all at once.
- await fetchA()
- await fetchB()
- await fetchC()
- fetchA()
- fetchB()
- fetchC()
const slow = await fetchA();
const slower = await fetchB();
const slowest = await fetchC();
const fast = await Promise.all([fetchA(), fetchB(), fetchC()]);That gap is not theoretical. It is the difference between a page that feels instant and a page that makes people wait for no reason.
Another Example: Dashboard Widgets
A dashboard is where this mistake becomes obvious. The profile card, notifications card, and stats card often come from different endpoints. If they do not depend on each other, there is no reason to load them in a line.
const profile = await fetchProfile();
const stats = await fetchStats();
const notices = await fetchNotices();Switch between Waterfall and Parallel and run it again. The work is identical. The user experience is not.
When Sequential Is Actually Correct
Parallel is not a religion. Some operations really are sequential:
const user = await fetchUser(userId);
const orders = await fetchOrders(user.id);Here, fetchOrders() needs user.id. Starting both together would be impossible or wrong. This is the part I care about most: make the dependency explicit. If the second line needs the first, await it. If it does not, start them together.
For a broader look at the syntax and error handling, see the full JavaScript async/await guide.
What I Check in Code Review
When I review async code, I look for one smell first: multiple await lines in a row.
Then I ask:
- Does each request need data from the previous request?
- Would
Promise.all()make this faster? - Should one failure cancel everything, or should partial results still render?
If one failure should not kill the whole screen, Promise.allSettled() may be a better fit. For example, a broken analytics widget should not stop a profile card from rendering.
FAQs
Common questions about sequential and parallel execution with async/await.
await is correct when each step depends on the result of the previous step. It becomes a performance bug when independent work is forced to wait in line.Promise.all() when several async operations can start at the same time and you need all of them to succeed before continuing.Promise.all() starts async work together, but JavaScript still runs on the same main thread. The speedup usually comes from overlapping waiting time, such as network requests.Promise.all() rejects as soon as one input rejects. If you want every operation to finish and then inspect successes and failures separately, use Promise.allSettled().Promise.all() for small independent groups. For large batches, add a concurrency limit.Summary
The biggest async/await performance mistake is not using await. It is waiting in sequence when the work could have started together.
Ask one question before every await: does this line need the result from the previous line? If it does, keep it sequential. If it does not, start the work together and wait with Promise.all().

