Sequential vs parallel
Learn when async work must happen one step at a time, when independent promises should run together, and how concurrency limits keep fast code friendly.
- 01Spot accidental waitingFind await-in-loop waterfalls that do not depend on previous results.
- 02Start independent work togetherUse
Promise.allwithawaitand know its fail-fast behavior. - 03Control pressureUse a small concurrency pool when unlimited parallel work would be rude or heavy.
The waiting choice
INTERACTIVEAsync JavaScript is not just about can this wait? It is also about when should I start the work? Sequential code waits in a line. Parallel code starts independent work together and waits for the group. A concurrency limit starts a few at a time.
A precise one-line definition: sequential async work starts the next operation after the previous one settles; parallel async work starts independent operations before waiting for all of them; limited concurrency runs a fixed number at once.
Imagine three laundry loads. One machine means load two cannot even begin until load one is out. Three machines let everything spin at once. A busy laundromat may allow only two machines per person, so the third load waits for a lane.
- In real life: One washer handles three loads one after another
- In JavaScript:
for...ofwithawaittakes roughly the sum of the waits - In real life: Three washers run at the laundromat
- In JavaScript:
Promise.alltends toward the longest wait - In real life: The laundromat only lets you use two washers
- In JavaScript: A concurrency limit keeps only two tasks in flight
Where the analogy stops: JavaScript is not doing the washing itself. The host, network, timer, disk, or server does the waiting work; JavaScript just starts it and resumes later.
const requests = [1200, 600, 900, 300];
// Sequential: next request starts after the previous one finishes.
for (const ms of requests) {
await fakeRequest(ms);
}
// Parallel: start every request first, then wait for all of them.
await Promise.all(requests.map((ms) => fakeRequest(ms)));
// Limited: at most two workers pull work from the list.
await mapWithLimit(requests, 2, fakeRequest);Run a mode to draw Gantt-style bars.
Total: not measured yet
Choose durations, then compare the three waiting styles.
setTimeout timers. Sequential tends toward the sum; parallel tends toward the longest; limit 2 lands between them.await in loops
STEP THROUGHawait inside for...of is sequential by design. That is perfect when each step needs the previous result, when order matters, or when an API asks you not to send too many requests. It is slow by accident when every item is independent.
The trap is forEach. It calls your async callback, but it ignores the promise returned by that callback. The outer function does not wait for those promises. Switch the setting below and watch the log order change.
Choose a loop style, then step through the real ordering consequence.
script
items.forEach(async (item) => { await save(item); console.log("saved " + item);});console.log("done");Use for...of when you intentionally want one-at-a-time behavior. Use await Promise.all(items.map(...)) when the items can run together. Do not expect forEach to await async callbacks.
Promise.all with await
GROUP WAITarray.map(async ...) returns an array of promises. That array is useful only if something waits for it. The common shape is await Promise.all(promises): start every independent operation, then pause until the group fulfills.
const promises = ids.map(async (id) => { return getProfile(id);}); // promises is an array. This is the promise for the whole group.const profiles = await Promise.all(promises);Promise.all fails fast: if one promise rejects, the group rejects as soon as that rejection is observed. It does not cancel other operations that already started. The upcoming Cancellation with AbortController lesson teaches cancellation. When you want every outcome, including failures, use Promise.allSettled and inspect the status objects.
Tea and toast do not depend on each other. Start both, then wait until both are ready.
- In real life: Boil the kettle
- In JavaScript: Call one function to start its promise
- In real life: Put bread in the toaster right away
- In JavaScript: Call the other function before awaiting
- In real life: Wait until both are ready
- In JavaScript:
await Promise.all([tea, toast])
Where the analogy stops: Creating a promise starts only the operation that function is written to start. Lazy APIs or async iterables may behave differently, so read the API you call.
Start early, await late
MEASUREawait getA(); await getB(); starts B after A finishes. If A and B are independent, create both promises first. The phrase to remember is: creating a promise starts the work; await decides when you stop and wait.
async function sequential() {
const a = await getA();
const b = await getB();
return [a, b];
}
async function startEarly() {
const aPromise = getA();
const bPromise = getB();
const a = await aPromise;
const b = await bPromise;
return [a, b];
}
async function safest() {
return Promise.all([getA(), getB()]);
}- Pick a strategy. A takes 700ms; B takes 400ms.
Creating the promise starts the host work; await decides when this async function pauses. Promise.all also wires rejection handling immediately.
Promise.all when another promise may reject first.There is a professional gotcha. In the manual const bPromise = getB(); await aPromise; await bPromise; shape, bPromise may reject while you are still awaiting aPromise. Some browsers may report that momentarily as an unhandled rejection. Promise.all wires the group together immediately, so it is usually the safer expression for independent work.
Concurrency limits
POOLStarting everything at once is not always kind. Browsers often limit simultaneous connections per server, historically around six for HTTP/1.1, while HTTP/2 can multiplex requests differently. Servers may rate-limit you. Your own page may run out of memory if you start a thousand image jobs.
A pool lets independent work stay independent while capping pressure. The implementation below starts a few workers. Each worker pulls the next index from shared state, awaits that item, and stores the result back into the original input order.
async function mapWithLimit(items, limit, fn) { const results = new Array(items.length); let nextIndex = 0; async function worker() { while (nextIndex < items.length) { const index = nextIndex++; results[index] = await fn(items[index], index); } } const workers = Array.from( { length: Math.min(limit, items.length) }, () => worker() ); await Promise.all(workers); return results;}A limited pool may finish item 3 before item 2, but it can still write results[index] so the returned array matches the input order.
Waterfalls
SORTA waterfall is a chain where each async step waits for the previous step. Some waterfalls are real. User → posts → comments is dependent: you need the user before their posts, and a post before its comments. Others are accidental, like calling Ann, waiting for Ann, and only then calling Ben even though Ben did not need Ann’s answer.
A to-do list can require one step before the next. Check whether the next line truly needs the value above it before making it wait.
- In real life: Finish step one before step two
- In JavaScript:
await stepOne()beforestepTwo() - In real life: Start unrelated tasks together
- In JavaScript: Start independent promises together
- In real life: Step two does not need step one
- In JavaScript: Waiting created an accidental waterfall
Where the analogy stops: If step two needs the result of step one, waiting is correct. The problem is waiting without a dependency.
// Dependent waterfall: each line needs data from the previous one.const user = await getUser(userId);const posts = await getPosts(user.id);const comments = await getComments(posts[0].id); // Independent requests: start together.const [profile, settings, notifications] = await Promise.all([ getProfile(userId), getSettings(userId), getNotifications(userId),]);- Fetch posts after fetching the user id from
/me - Load profile, settings, and notifications for the same known user
- Run migration 2 after migration 1 changed the schema
- Create thumbnails for ten unrelated images
- Ask for an access token, then call the protected API with it
- Fetch pricing, reviews, and inventory for a product card
Sort each real-world step by whether it has a true dependency.
Where you’ll use this
You will make this choice in route loaders, dashboards, upload tools, migrations, form submit handlers, and build scripts. Ask three questions before adding await above another await:
- Does the next operation need the previous value?
- Would starting everything overwhelm a server, browser, or memory?
- Do I need all successes, the first failure, or every settled result?
| Pattern | Best for | Watch out for |
|---|---|---|
await inside for...of | Order-dependent steps, rate limits, previous result needed | Accidental waterfalls when each item is independent |
await Promise.all(...) | Independent work where all successes are required | Fails fast and does not cancel other started operations |
Promise.allSettled(...) | Collecting every success and failure | You must inspect each result object yourself |
| Concurrency pool | Many independent tasks with server, memory, or browser limits | More code; choosing a limit needs judgement |
Common misconceptions
- “await blocks JavaScript.” It pauses the current async function; the event loop can keep doing other work.
- “Parallel means CPU threads.” Here it means overlapping host work like timers, network, and file operations while JavaScript waits.
- “Promise.all cancels losers.” It rejects fast but does not cancel other operations. Use cancellation APIs when cancellation matters.
- “map async gives values.” It gives promises. Await the group to get values.
- “Sequential is always bad.” It is correct for true dependencies, ordered side effects, and rate limits.
- “Unlimited parallel is always fastest.” Resource limits can make it slower or unreliable.
Practice exercises
5 EXERCISESRead the code. What is the first console line?
async function save(item) {
console.log("start " + item);
}
["A", "B"].forEach(async (item) => {
await save(item);
console.log("saved " + item);
});
console.log("done");save('A') logs start A before its promise is awaited, so that is the first line.
The app shows “done” too early. Does it print before or after the first saved item?
const items = ["A", "B", "C"];
items.forEach(async (item) => {
await save(item);
console.log("saved " + item);
});
console.log("done");const items = ["A", "B", "C"];
await Promise.all(
items.map(async (item) => {
await save(item);
console.log("saved " + item);
})
);
console.log("done");Replace forEach with await Promise.all(items.map(async ...)) when all saves are independent and the final done must wait.
What wrapper waits for promises returned by map(async ...)?
async function double(n) { return n * 2; }
const values = [1, 2].map(async (n) => double(n));
console.log(Array.isArray(values));
console.log(typeof values[0].then);const doubled = await Promise.all(numbers.map(async (n) => n * 2));Promise.all turns the array of promises into one promise for the array of values.
Durations are 100ms, 50ms, and 70ms. A pool with limit 2 starts from the front. What is the rough total?
With limit 2, the 100ms and 50ms jobs start first. The 70ms job starts when the 50ms job frees a lane, so the total is about 120ms.
You are importing 100 rows and want a report for every row, even failed ones. Which tool collects every result?
async function mapWithLimit(items, limit, fn) {
const results = new Array(items.length);
let nextIndex = 0;
async function worker() {
while (nextIndex < items.length) {
const index = nextIndex++;
results[index] = await fn(items[index], index);
}
}
const workers = Array.from(
{ length: Math.min(limit, items.length) },
() => worker()
);
await Promise.all(workers);
return results;
}Use Promise.allSettled when you need every success and failure. You can combine it with a pool by making each worker catch or settle its own item.
Quiz
7 QUESTIONSQuestion 1 of 7What does
awaitin afor...ofloop do?Choose an answer to see the explanation.
Question 2 of 7What prints first?
Read the code, then predictasync function save(item) { console.log("saved " + item); } ["A"].forEach(async (item) => { await save(item); }); console.log("done");Choose an answer to see the explanation.
Question 3 of 7What does
array.map(async fn)return immediately?Choose an answer to see the explanation.
Question 4 of 7What does
Promise.alldo when one promise rejects?Choose an answer to see the explanation.
Question 5 of 7Which version avoids an unnecessary waterfall?
Read the code, then predictfunction getA() { return Promise.resolve("a"); } function getB() { return Promise.resolve("b"); } const a = getA(); const b = getB(); Promise.all([a, b]).then((values) => { console.log(values.length); });Choose an answer to see the explanation.
Question 6 of 7Why might you use a concurrency limit for independent work?
Choose an answer to see the explanation.
Question 7 of 7When is sequential code the right choice?
Choose an answer to see the explanation.
Key takeaways
awaitinfor...ofis intentionally sequential.forEachdoes not wait for async callbacks.- Start independent promises before awaiting, usually with
Promise.all. Promise.allfails fast but does not cancel work already started.- Use
Promise.allSettledwhen every result matters. - Use a concurrency limit when unlimited starts would overload resources.
Sequential waits in line; parallel starts together; a pool starts a sensible number together.
Up next: Cancellation with AbortController.