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 waiting you don't needFind an
awaitin a loop that makes jobs wait in line when they don't need each other's results. - 02Start separate jobs togetherUse
Promise.allwithawait, and know that it fails as soon as one job fails. - 03Run only a few at a timeUse a small pool when starting every job at once would be too much for a server or the browser.
Three ways to wait
INTERACTIVESome work in JavaScript finishes later, not right away. Loading data from a server is one example. A timer is another. We call this async work. When you start async work, you get back a promise: an object that will hold the result once the work is done. When the work is done, we say the promise has settled. It either succeeded or failed.
You already know that await pauses a function until a promise settles. This lesson asks a new question: when you have several jobs, when should each one start? There are three choices:
- Sequential: one job at a time. The next job starts only after the one before it has settled.
- Parallel: start all the jobs first, then wait until every one is done.
- Limited: run only a few jobs at the same time, for example two. This is called a concurrency limit. "Concurrency" just means "how many things are running at the same time".
async function save(item) {
console.log("saved " + item);
}
async function main() {
for (const item of ["tea", "toast"]) {
await save(item);
}
console.log("done");
}
main();Line 6 loops over two items, "tea" and "toast". Line 7 calls save and waits for it. The loop does not move on to "toast" until the save for "tea" has settled. So the output is saved tea, then saved toast, then done. This is sequential: the second save starts only on the next turn of the loop.
You have three loads of laundry. With one washer, load two can't start until load one is out. With three washers, all three loads wash at the same time. A busy laundromat may let each person use only two washers. Then the third load waits until a washer is free.
- In real life: One washer washes three loads, one after another
- In JavaScript:
awaitinsidefor...oftakes about the sum of all the waits - In real life: Three washers run at the same time
- In JavaScript:
Promise.alltakes about as long as the longest wait - In real life: The laundromat lets you use only two washers
- In JavaScript: A concurrency limit runs only two jobs at a time
const requests = [900, 500, 700, 300];
// Sequential: the next request starts after the one before it 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 take jobs from the list.
await mapWithLimit(requests, 2, fakeRequest);Press a button to draw one bar for each request.
Total: not measured yet
Set how long each request takes, then press a button to try one way to wait.
setTimeout timer. One at a time takes about the sum of all the times. Promise.all takes about as long as the longest one. A limit of 2 lands in between.await inside a loop
STEP THROUGHWhen you put await inside a for...of loop, the loop runs one item at a time. That is on purpose. It is the right choice when:
- each step needs the result of the step before it,
- the order of the steps matters, or
- a server asks you not to send too many requests at once.
It is slow for no reason when the items don't depend on each other. Then they wait in line when they could all run at once.
Watch out for forEach. It calls your async function once for each item. Each call gives back a promise, but forEach throws those promises away. So the code after forEach doesn't wait for them. Switch the setting below and watch the order of the printed lines change.
async function save(item) {
console.log("saved " + item);
}
async function main() {
for (const item of ["tea", "toast"]) {
await save(item);
}
console.log("done");
}
main();The two items are saved in array order, and done prints last. That order matters when, for example, request 2 must not start until request 1 has finished.
Pick a loop style, then step through to see the order of the printed lines.
script
items.forEach(async (item) => { await save(item); console.log("saved " + item);});console.log("done");forEach, nothing waits for the saves, so done prints first. With Promise.all, done waits until all three saves finish.Use for...of with await when you want one job at a time. Use await Promise.all(items.map(...)) when the jobs can run together. Don't use forEach with an async function if later code must wait for it.
Promise.all with await
GROUP WAITIf you call array.map with an async function, you get back an array of promises, not an array of results. To get the results, you need something that waits for all of them. That is Promise.all. You give it an array of promises, and it gives you back one promise for the whole group.
So the usual pattern is await Promise.all(promises). First you start every job. Then you pause once, until the whole group is done.
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);Lines 1 to 3 call getProfile once for each id. Each call starts right away and gives back a promise. Line 6 waits for all of them and puts the results in profiles.
async function load(name) {
return "loaded " + name;
}
async function main() {
const results = await Promise.all([
load("profile"),
load("settings"),
]);
console.log(results.join(" | "));
}
main();Each load call gives back a promise. Promise.all waits for both and gives back the two strings in an array. The output is loaded profile | loaded settings. The results are always in the same order as the calls, even if the second job finishes first.
Promise.all joins two promises into one. await waits for that one promise and gives back both values, in the same order as the calls.Promise.all fails fast. If one promise fails (we say it rejects), the whole group fails right away. You don't wait for the others. But Promise.all does not stop the other jobs. They keep running. The Cancellation with AbortController lesson, coming up next, shows how to stop them.
Sometimes you want every result, even the failed ones. Then use Promise.allSettled. It waits for every promise and gives you one small report object for each.
Making tea doesn't need the toast, and making toast doesn't need the tea. So you switch on the kettle, put the bread in the toaster, and then wait until both are ready.
- In real life: Switch on the kettle
- In JavaScript: Call one function. It starts its job and gives back a promise
- In real life: Put bread in the toaster right away
- In JavaScript: Call the other function before any
await - In real life: Wait until both are ready
- In JavaScript:
await Promise.all([tea, toast])
Start early, await late
MEASUREawait getA(); await getB(); starts getB only after getA has finished. If the two jobs don't depend on each other, call both functions first, and wait afterwards. Remember this sentence: calling the function starts the job; await only 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()]);
}- Press a button. Job A takes 700ms. Job B takes 400ms.
Calling a function starts its job. await only decides when this function pauses. Promise.all also listens for a failure from every job straight away.
Promise.all.There is one catch with the middle version in the lab, where you call both functions and then await each one. Look at the code: const bPromise = getB(); await aPromise; await bPromise;. bPromise might fail while the function is still waiting on aPromise. At that moment, nothing is listening for that failure yet. Some browsers then show an "unhandled rejection" warning in the console. Promise.all listens to every promise in the group from the start, so it is usually the safer choice.
async function main() {
const results = await Promise.allSettled([
Promise.resolve("saved A"),
Promise.reject("offline"),
]);
console.log(results.map((result) => result.status).join(", "));
}
main();The first promise succeeds and the second one fails. Promise.allSettled waits for both and gives one report object for each. Each object has a status: "fulfilled" for a success or "rejected" for a failure. So this prints fulfilled, rejected. Promise.all would have failed instead.
Limit how many run at once
POOLStarting every job at once is not always a good idea:
- Browsers open only a few connections to the same server at a time. With the older HTTP/1.1 protocol, it is often about six. Newer HTTP/2 can send many requests over one connection.
- A server may refuse your requests if you send too many too fast. This is called a rate limit.
- Your own page may run out of memory if it starts a thousand image jobs at once.
A pool solves this. The jobs still don't depend on each other, but only a few run at the same time. The code below makes a small number of workers. A worker is a function that repeats three steps:
- Take the next position (index) from the list.
- Wait for that item's job.
- Store the result at the same position, so the results stay in the same order as the input.
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;}async function main() {
const results = await mapWithLimit(
["A", "B", "C"],
2,
async (item) => item.toLowerCase(),
);
console.log(results.join(","));
}
main();With a limit of 2, the two workers can finish at different times. But each result goes into its own position. So this prints a,b,c, the same order as the input, not the order the workers finished in.
results is still a,b,c.A pool may finish item 3 before item 2. But because it writes results[index], the array it returns is still in input order.
Waterfalls: steps that wait for each other
SORTA waterfall is a chain of async steps where each step waits for the one before it. Some waterfalls are needed:
- You need the user before you can load their posts.
- You need a post before you can load its comments.
Other waterfalls happen by mistake. Imagine you phone Ann, wait for her answer, and only then phone Ben. Ben never needed Ann's answer, so the wait was wasted time.
On a to-do list, some steps must wait for the step before. You can't paint a wall before you build it. Other steps don't: you can paint the wall while the laundry washes. Before you make a step wait, check that it really needs the step above it.
- In real life: Finish step one before step two
- In JavaScript:
await stepOne()beforestepTwo() - In real life: Start unrelated tasks together
- In JavaScript: Start promises that don't depend on each other together
- In real life: Step two never needed step one
- In JavaScript: The wait made a waterfall by mistake
// A real waterfall: each request needs data from the one before.const user = await getUser(userId);const posts = await getPosts(user.id);const comments = await getComments(posts[0].id); // These requests don't need each other: start them together.const [profile, settings, notifications] = await Promise.all([ getProfile(userId), getSettings(userId), getNotifications(userId),]);Lines 2 to 4 are a real waterfall: getPosts needs user.id from line 2, and getComments needs a post's id from line 3. Lines 7 to 11 don't: all three requests only need userId, which you already have. So they start together.
async function main() {
const user = { id: 7 };
const posts = await getPosts(user.id);
console.log("posts need " + posts[0].id);
const profilePromise = getProfile(user.id);
const settingsPromise = getSettings(user.id);
const [profile, settings] = await Promise.all([
profilePromise,
settingsPromise,
]);
console.log(profile.name + " / " + settings.theme);
}
main();Line 3 waits for the posts, because line 4 needs them. The profile and the settings only need user.id, and neither needs the other. So lines 6 and 7 start both, and line 8 waits for the pair. This prints posts need 70, then Ada / dark.
- Load the user's posts, after asking the server
/mefor the user's id - Load the profile, settings, and notifications of a user whose id you already have
- Run database update 2 after database update 1 has changed the tables
- Make small preview images (thumbnails) for ten separate photos
- Ask for a login token, then call a server that only answers when you send that token
- Load the price, reviews, and stock count for one product
Sort each step: does it really need the result of an earlier step?
Where you’ll use this
You will make this choice often. For example, when a page loads its data, when a dashboard fills its panels, when a tool uploads many files, or when a form is sent. Before you put one await under another, ask three questions:
- Does the next job need the result of the one before it?
- Would starting every job at once be too much for the server, the browser, or memory?
- Do I need every job to succeed, or do I want a report on every job, even the failed ones?
The table below compares the four ways to wait. Read each row as: "this pattern is good for this, but watch out for that."
| Pattern | When it's a good fit | What to watch out for |
|---|---|---|
await inside for...of | Each step needs the step before it, the order matters, or a server limits how fast you can send. | If the items don't need each other, they wait in line for no reason. |
await Promise.all(...) | Jobs that don't need each other, when every job must succeed. | It fails as soon as one job fails, and it doesn't stop the other jobs. |
Promise.allSettled(...) | You want a report on every job, success or failure. | You must check each report's status yourself. |
| A pool with a limit | Many jobs that don't need each other, but too many to start at once. | It takes more code, and you have to pick a sensible limit. |
async function main() {
const userId = 3;
const [profile, settings] = await Promise.all([
getProfile(userId),
getSettings(userId),
]);
console.log(profile.name + " / " + settings.theme);
}
main();The profile and the settings both only need userId, so neither waits for the other. The dashboard waits once for both and prints Ada / dark.
Common misconceptions
- “await freezes all of JavaScript.” It only pauses the async function it is in. Other code, like click handlers, can still run.
- “Parallel means using several CPU cores.” Here it means the waits overlap. The browser or the network does the work while your JavaScript waits.
- “Promise.all stops the other jobs when one fails.” It fails right away, but the other jobs keep running. You need a separate way to cancel them.
- “map with an async function gives me the results.” It gives an array of promises. Wait for them with
Promise.allto get the results. - “One at a time is always bad.” It is correct when each step needs the one before, when order matters, or when a server limits your requests.
- “Starting everything at once is always fastest.” Too many jobs at once can overload the server or the browser and make things slower or fail.
Array.fromAsync builds an array from values that arrive one by one. It waits for each value before it asks for the next. So if you already have an array of jobs that can run together, use Promise.all instead.
Practice exercises
5 EXERCISESRead the code. What is the first line it prints?
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') prints start A as soon as it is called, before anything waits. So start A is the first line.
The app shows “done” too early. Does done print before or after saved A?
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");forEach doesn't wait, so done prints before saved A. To fix it, replace forEach with await Promise.all(items.map(async ...)). The saves don't depend on each other, and now done waits for all of them.
This code prints true and function: values is an array of promises. What do you wrap around it to wait for the results?
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. When you await it, you get the array of results.
Three jobs take 100ms, 50ms, and 70ms. A pool with a limit of 2 starts them in that order. About how long does it take in total?
With a limit of 2, the 100ms and 50ms jobs start first. The 50ms job ends at 50ms, so the 70ms job starts then and ends at 120ms. The 100ms job ended at 100ms. So the total is about 120ms.
You are importing 100 rows. You want a report for every row, even the ones that fail. Which tool gives you 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 every failure. You can also use it with a pool: make each job catch its own error and return a report.
Quiz
7 QUESTIONSQuestion 1 of 7What does
awaitinside afor...ofloop do?Choose an answer to see the explanation.
Question 2 of 7What does this print 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)give back straight away?Choose an answer to see the explanation.
Question 4 of 7What does
Promise.alldo when one of its promises fails?Choose an answer to see the explanation.
Question 5 of 7Both jobs start before the wait. What does this print?
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 7The jobs don't need each other. Why would you still limit how many run at once?
Choose an answer to see the explanation.
Question 7 of 7When is running jobs one at a time the right choice?
Choose an answer to see the explanation.
Key takeaways
awaitinsidefor...ofruns one job at a time, on purpose.forEachdoesn't wait for async functions.- Start jobs that don't depend on each other first, then wait for them together, usually with
Promise.all. Promise.allfails as soon as one job fails, but it doesn't stop the other jobs.- Use
Promise.allSettledwhen you need every result, even failures. - Use a pool when starting every job at once would be too much.
Sequential waits in line. Parallel starts everything together. A pool starts a few at a time.
Up next: Cancellation with AbortController.