cf.completefrontendCode editorOpen lab
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 accidental waitingFind await-in-loop waterfalls that do not depend on previous results.
  • 02
    Start independent work togetherUse Promise.all with await and know its fail-fast behavior.
  • 03
    Control pressureUse a small concurrency pool when unlimited parallel work would be rude or heavy.

The waiting choice

INTERACTIVE

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

Real-life analogyLaundry day at three speeds

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...of with await takes roughly the sum of the waits
In real life: Three washers run at the laundromat
In JavaScript: Promise.all tends 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.

Timeline lab: one machine, many machines, or two lanes?
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);
Durations and measured bars

Run a mode to draw Gantt-style bars.

Total: not measured yet

Try it yourself

Choose durations, then compare the three waiting styles.

The fake requests use real setTimeout timers. Sequential tends toward the sum; parallel tends toward the longest; limit 2 lands between them.

await in loops

STEP THROUGH

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

forEach does not wait for async callbacks
Step 0 of 9Ready
Your turn: follow the blue line

Choose a loop style, then step through the real ordering consequence.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
items.forEach(async (item) => {  await save(item);  console.log("saved " + item);});console.log("done");
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Switch the looping line
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.
Tiny rule

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 WAIT

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

Map starts the work; 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);

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.

Real-life analogyTea and toast: wait or start together

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

MEASURE

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

Start early, await late
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()]);
}
Measured log
  1. Pick a strategy. A takes 700ms; B takes 400ms.
Try it yourself

Creating the promise starts the host work; await decides when this async function pauses. Promise.all also wires rejection handling immediately.

The rejection toggle demonstrates why start-early/await-late is less safe than 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

POOL

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

A small concurrency poolPop 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;}
Order of results vs order of finishing

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

SORT

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

Real-life analogyA to-do list with dependent steps

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() before stepTwo()
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 vs independent requestsPop out in the code editor (opens in a new tab)js
// 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),]);
Must wait or can run together?
  • 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
Try it yourself
0 of 6 correct

Sort each real-world step by whether it has a true dependency.

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 in route loaders, dashboards, upload tools, migrations, form submit handlers, and build scripts. Ask three questions before adding await above another await:

  1. Does the next operation need the previous value?
  2. Would starting everything overwhelm a server, browser, or memory?
  3. Do I need all successes, the first failure, or every settled result?
Choosing a waiting pattern
PatternBest forWatch out for
await inside for...ofOrder-dependent steps, rate limits, previous result neededAccidental waterfalls when each item is independent
await Promise.all(...)Independent work where all successes are requiredFails fast and does not cancel other started operations
Promise.allSettled(...)Collecting every success and failureYou must inspect each result object yourself
Concurrency poolMany independent tasks with server, memory, or browser limitsMore 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 EXERCISES
Exercise 1 · Warm-upPredict the first log

Read the code. What is the first console line?

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 it print before or after the first saved item?

    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 mapped promises

      What wrapper waits for promises returned by map(async ...)?

      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 · PracticeEstimate a limit

        Durations are 100ms, 50ms, and 70ms. A pool with limit 2 starts from the front. What is the rough total?

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

          Exercise 5 · ChallengeChoose the right combinator

          You are importing 100 rows and want a report for every row, even failed ones. Which tool collects 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 in a for...of loop do?

              Choose an answer to see the explanation.

            2. Question 2 of 7What prints 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) return immediately?

              Choose an answer to see the explanation.

            4. Question 4 of 7What does Promise.all do when one promise rejects?

              Choose an answer to see the explanation.

            5. Question 5 of 7Which version avoids an unnecessary waterfall?

              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 7Why might you use a concurrency limit for independent work?

              Choose an answer to see the explanation.

            7. Question 7 of 7When is sequential code the right choice?

              Choose an answer to see the explanation.

            Key takeaways

            • await in for...of is intentionally sequential.
            • forEach does not wait for async callbacks.
            • Start independent promises before awaiting, usually with Promise.all.
            • Promise.all fails fast but does not cancel work already started.
            • Use Promise.allSettled when 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.

            CompleteFrontend Clear concepts. Working examples.