completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

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.

By the end, you can
  • 01
    Spot waiting you don't needFind an await in a loop that makes jobs wait in line when they don't need each other's results.
  • 02
    Start separate jobs togetherUse Promise.all with await, and know that it fails as soon as one job fails.
  • 03
    Run 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

INTERACTIVE

Some 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.

Real-life analogyLaundry day at three speeds

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: await inside for...of takes about the sum of all the waits
In real life: Three washers run at the same time
In JavaScript: Promise.all takes 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
Timeline lab: three ways to wait
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);
Request times and the bars they draw

Press a button to draw one bar for each request.

Total: not measured yet

Try it yourself

Set how long each request takes, then press a button to try one way to wait.

Each fake request is a real 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 THROUGH

When 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.

A loop that waits on purposePop out in the code editor (opens in a new tab)js
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.

forEach doesn't wait for async functions
Step 0 of 9Ready
Your turn: follow the blue line

Pick a loop style, then step through to see the order of the printed lines.

Running in
  1. script
Next: line 1
items.forEach(async (item) => {  await save(item);  console.log("saved " + item);});console.log("done");
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Pick how the loop is written
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Two consoles. Left, forEach: done is printed first, then saved A, saved B, saved C. Right, Promise.all with map: saved A, saved B, saved C, then done is printed last.forEachdonesaved Asaved Bsaved Cdone too earlyPromise.allsaved Asaved Bsaved Cdonedone waits
With forEach, nothing waits for the saves, so done prints first. With Promise.all, done waits until all three saves finish.
A simple rule

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 WAIT

If 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.

map starts the jobs; Promise.all waits for the groupPop out in the code editor (opens in a new tab)js
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.

The result is still an arrayPop out in the code editor (opens in a new tab)js
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.

Two Promise cards go into Promise.all, which gives one Promise card. await turns that one promise into two values: loaded profile and loaded settings.PromisePromise2 promisesPromise.allPromise1 promiseawait"loaded profile""loaded settings"2 values
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.

Real-life analogyTea and toast: wait or start together

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

MEASURE

await 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.

Start early, await late
Three ways to call A and BPop out in the code editor (opens in a new tab)js
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()]);
}
What happened, and when
  1. Press a button. Job A takes 700ms. Job B takes 400ms.
Try it yourself

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.

Turn on "B fails" to see why calling both and then awaiting each one is less safe than Promise.all.
Top: await getA, then await getB. A takes 700 ms, then B takes 400 ms, so the total is 1100 ms. Bottom: Promise.all of getA and getB. A and B start together, so the total is 700 ms, the time of the longer one.await getA(); await getB();A: 700 msB: 400 ms1100 msPromise.all([getA(), getB()])A: 700 msB: 400 ms700 ms
A longer card means a longer wait. One after the other, the waits add up: 700 + 400 = 1100 ms. Started together, you only wait for the longer one: 700 ms.

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.

Every result with allSettledPop out in the code editor (opens in a new tab)js
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

POOL

Starting 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:

  1. Take the next position (index) from the list.
  2. Wait for that item's job.
  3. Store the result at the same position, so the results stay in the same order as the input.
A small pool that runs only a few jobs at oncePop out in the code editor (opens in a new tab)js
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 pool keeps the input orderPop out in the code editor (opens in a new tab)js
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.

Two worker boxes. Worker 1 handles A and C. Worker 2 handles B. Arrows go from both workers to a results cart that holds a, b, c in the same order as the input.ACworker 1Bworker 2abcresultsinput order kept
With a limit of 2, only two workers run. Worker 1 does A and then C; worker 2 does B. Each result goes into its own slot, so results is still a,b,c.
Order of results vs order of finishing

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

SORT

A 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.

Real-life analogyA to-do list with steps that depend on each other

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() before stepTwo()
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
Requests that need each other, and requests that don'tPop out in the code editor (opens in a new tab)js
// 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.

Check what each step needs before you choosePop out in the code editor (opens in a new tab)js
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.

Must wait or can run together?
  • Load the user's posts, after asking the server /me for 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
Try it yourself
0 of 6 correct

Sort each step: does it really need the result of an earlier step?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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:

  1. Does the next job need the result of the one before it?
  2. Would starting every job at once be too much for the server, the browser, or memory?
  3. 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."

Which way to wait for several jobs
PatternWhen it's a good fitWhat to watch out for
await inside for...ofEach 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 limitMany 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.
A dashboard loads two panels togetherPop out in the code editor (opens in a new tab)js
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.all to 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.
A related tool: Array.fromAsync

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 EXERCISES
Exercise 1 · Warm-upPredict the first line

Read the code. What is the first line it prints?

Starter codePop out in the code editor (opens in a new tab)JavaScript
async function save(item) {
  console.log("start " + item);
}
["A", "B"].forEach(async (item) => {
  await save(item);
  console.log("saved " + item);
});
console.log("done");

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeFind the bug

    The app shows “done” too early. Does done print before or after saved A?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const items = ["A", "B", "C"];
    items.forEach(async (item) => {
      await save(item);
      console.log("saved " + item);
    });
    console.log("done");

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeWait for the promises from map

      This code prints true and function: values is an array of promises. What do you wrap around it to wait for the results?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      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);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeWork out the time with a limit

        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?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeChoose the right tool

          You are importing 100 rows. You want a report for every row, even the ones that fail. Which tool gives you every result?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          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;
          }

          Answer, then press Check. Spacing and letter case don’t matter.

            Quiz

            7 QUESTIONS
            Sequential vs parallel quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What does await inside a for...of loop do?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this print first?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              async function save(item) {
                console.log("saved " + item);
              }
              ["A"].forEach(async (item) => {
                await save(item);
              });
              console.log("done");

              Choose an answer to see the explanation.

            3. Question 3 of 7What does array.map(async fn) give back straight away?

              Choose an answer to see the explanation.

            4. Question 4 of 7What does Promise.all do when one of its promises fails?

              Choose an answer to see the explanation.

            5. Question 5 of 7Both jobs start before the wait. What does this print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function 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.

            6. 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.

            7. Question 7 of 7When is running jobs one at a time the right choice?

              Choose an answer to see the explanation.

            Key takeaways

            • await inside for...of runs one job at a time, on purpose.
            • forEach doesn'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.all fails as soon as one job fails, but it doesn't stop the other jobs.
            • Use Promise.allSettled when 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.

            CompleteFrontend Clear concepts. Working examples.