Skip to content
aviral gupta

// B4.3 · ~32 min · Beginner

Sequential and parallel

After this lesson you can run async operations one after another or all at once, choose between Promise.all and Promise.allSettled when some fail, and cap how many run at the same time.

Lesson 3 of 5 in B4 Asynchronous Node

You will be able to

  • Run async operations in sequence with await in a loop, or in parallel with Promise.all
  • Handle failures in a group: Promise.all, Promise.allSettled, and briefly any and race
  • Limit concurrency with a small pool, and measure it by counting, not by the clock
  1. Warm-up · Activity 1 of 7

    Warm-up from B4.2: the callback is async now, so await is allowed in it. What does the last line print?

    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    const done = [];
    
    [30, 10, 20].forEach(async (ms) => {
      await sleep(ms);
      done.push(ms);
    });
    console.log(done);
  2. Predict · Activity 2 of 7

    Predict before you read on. Task a takes 60 ms, task b takes 20 ms, and both go into Promise.all. What does the program print?

    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    
    async function task(name, ms) {
      console.log('start', name);
      await sleep(ms);
      console.log('end', name);
      return name;
    }
    
    console.log(await Promise.all([task('a', 60), task('b', 20)]));
  3. Practice · Activity 3 of 7

    Fill in the method that waits for every promise, even after one rejects, and reports each outcome.

    const results = await Promise.____([
      Promise.resolve('a.txt'),
      Promise.reject(new Error('ENOENT')),
      Promise.resolve('c.txt')
    ]);
    console.log(results.map((result) => result.status));
    const results = await Promise.([
  4. Practice · Activity 4 of 7

    Three promises: slow (30 ms) fulfils, fast (10 ms) rejects, medium (20 ms) fulfils. Match each method with what it does.

  5. Practice · Activity 5 of 7

    fail rejects after 10 ms; slow takes 50 ms. What does the program print?

    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    
    const slow = async () => {
      await sleep(50);
      console.log('slow done');
    };
    const fail = async () => {
      await sleep(10);
      throw new Error('boom');
    };
    
    try {
      await Promise.all([slow(), fail()]);
    } catch (error) {
      console.log('caught', error.message);
    }
  6. Brain teaser · Activity 6 of 7

    Brain teaser. Two workers share one list of four tasks: a takes 100 ms, b, c and d take 20 ms each. In which order do the tasks end?

    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    const tasks = [['a', 100], ['b', 20], ['c', 20], ['d', 20]];
    let next = 0;
    
    async function worker() {
      while (next < tasks.length) {
        const [name, ms] = tasks[next++];
        await sleep(ms);
        console.log('end', name);
      }
    }
    
    await Promise.all([worker(), worker()]);
  7. Apply · Activity 7 of 7

    Mini-task. In main.js, list the .txt files of the folder notes with readdir from node:fs/promises, sorted by name. Count the words of each file with at most two reads at once, using a pool you write yourself, and print one line per file, name: N words, in name order, then the total. Put three or four small .txt files in notes and run node main.js.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Read three files in sequence, in parallel and in a pool

read() reads one file and counts how many reads are running at the same moment; most keeps the highest count. The program reads the same three files one after another, all at once with Promise.all, and with a pool of two. Then it reads a list with a missing file, first with Promise.allSettled, which reports each file, and then with Promise.all, which rejects. The counts and the order are the same on every run; the times would not be.

main.js

import {readFile} from 'node:fs/promises';

let active = 0; // reads running right now
let most = 0; // the most that ran at the same time

// Reads one file, and counts how many reads run at once.
async function read(name) {
  active++;
  most = Math.max(most, active);
  try {
    return (await readFile(name, 'utf8')).trim();
  } finally {
    active--;
  }
}

// At most limit calls of fn at once; results in input order.
async function mapLimit(items, limit, fn) {
  const results = [];
  let next = 0;
  async function worker() {
    while (next < items.length) {
      const index = next++;
      results[index] = await fn(items[index]);
    }
  }
  await Promise.all(Array.from({length: limit}, worker));
  return results;
}

const names = ['a.txt', 'b.txt', 'c.txt'];

most = 0;
const one = [];
for (const name of names) one.push(await read(name));
console.log('in sequence:', one, '| at most', most, 'at once');

most = 0;
const all = await Promise.all(names.map(read));
console.log('in parallel:', all, '| at most', most, 'at once');

most = 0;
const pooled = await mapLimit(names, 2, read);
console.log('pool of 2:', pooled, '| at most', most, 'at once');

const withMissing = ['a.txt', 'missing.txt', 'c.txt'];
const settled = await Promise.allSettled(withMissing.map(read));
settled.forEach((result, i) => {
  console.log(withMissing[i], result.status, result.value ?? result.reason.code);
});

try {
  await Promise.all(withMissing.map(read));
} catch (error) {
  console.log('Promise.all rejected:', error.code);
}

a.txt

alpha

b.txt

beta

c.txt

gamma

Run it with

node main.js

Output

in sequence: [ 'alpha', 'beta', 'gamma' ] | at most 1 at once
in parallel: [ 'alpha', 'beta', 'gamma' ] | at most 3 at once
pool of 2: [ 'alpha', 'beta', 'gamma' ] | at most 2 at once
a.txt fulfilled alpha
missing.txt rejected ENOENT
c.txt fulfilled gamma
Promise.all rejected: ENOENT
  • The loop awaits each read before it starts the next, so never more than one runs.
  • map calls read three times before anything is awaited, so all three run at once.
  • Array.from({length: 2}, worker) starts two workers; they share next, so each file is read once.
  • All three ways give the texts in the order of names.
  • allSettled reports the two good files and the ENOENT; Promise.all gives only the error.

Exercises

Exercise 1 of 2

A pool with a limit

mapLimit(items, limit, fn) calls fn(item) for every item and returns the results in the order of items. The starter starts every call at once. Change it so that at most limit calls run at the same time: start limit workers (or fewer, if there are fewer items), and let each worker take the next index until none are left. This part runs in the browser too: slowDouble is a stand-in for a slow read.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Keep let next = 0; and a results array outside the workers. A worker loops while (next < items.length) and takes const index = next++.

  2. Hint 2

    Store the result with results[index] = await fn(items[index]); so the order follows items, not finishing time.

  3. Hint 3

    Start Math.min(limit, items.length) workers in an array, then await Promise.all(workers) and return results.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

// Calls fn(item) for every item, with at most limit calls running at once.
// The results come back in the order of items, not in the order they finish.
export async function mapLimit(items, limit, fn) {
  const results = [];
  let next = 0; // the index of the next item to start
  async function worker() {
    while (next < items.length) {
      const index = next++;
      results[index] = await fn(items[index]);
    }
  }
  const workers = [];
  for (let i = 0; i < Math.min(limit, items.length); i++) workers.push(worker());
  await Promise.all(workers);
  return results;
}

// A stand-in for slow work: waits ms milliseconds, then doubles ms.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let active = 0;
let most = 0;
const slowDouble = async (ms) => {
  active++;
  most = Math.max(most, active);
  await sleep(ms);
  active--;
  return ms * 2;
};

console.log(await mapLimit([30, 10, 20, 10], 2, slowDouble));
console.log('at most', most, 'at once');
Run it on your computer

Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

// Calls fn(item) for every item, with at most limit calls running at once.
// The results come back in the order of items, not in the order they finish.
export async function mapLimit(items, limit, fn) {
  return Promise.all(items.map(fn)); // starts every call at once
}

// A stand-in for slow work: waits ms milliseconds, then doubles ms.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let active = 0;
let most = 0;
const slowDouble = async (ms) => {
  active++;
  most = Math.max(most, active);
  await sleep(ms);
  active--;
  return ms * 2;
};

console.log(await mapLimit([30, 10, 20, 10], 2, slowDouble));
console.log('at most', most, 'at once');

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {mapLimit} from './main.js';

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// fn for the tests: counts the calls running at once, and keeps the highest count.
function counter() {
  const seen = {active: 0, most: 0, calls: 0};
  seen.fn = async (ms) => {
    seen.calls++;
    seen.active++;
    seen.most = Math.max(seen.most, seen.active);
    await sleep(ms);
    seen.active--;
    return ms * 2;
  };
  return seen;
}

test('returns the results in the order of items', async () => {
  const got = await mapLimit([30, 10, 20], 2, counter().fn);
  assert.deepEqual(got, [60, 20, 40], `mapLimit returned ${JSON.stringify(got)}`);
});

test('never runs more than limit calls at once', async () => {
  const seen = counter();
  await mapLimit([20, 10, 30, 10, 20, 10], 2, seen.fn);
  assert.equal(seen.most, 2, `at most ${seen.most} calls ran at once; expected 2`);
});

test('calls fn once for every item, also when limit is larger than the list', async () => {
  const seen = counter();
  const got = await mapLimit([10, 20], 5, seen.fn);
  assert.deepEqual(got, [20, 40], `mapLimit returned ${JSON.stringify(got)}`);
  assert.equal(seen.calls, 2, `fn was called ${seen.calls} times; expected 2`);
});

test('an empty list gives an empty array', async () => {
  const got = await mapLimit([], 3, counter().fn);
  assert.deepEqual(got, [], `mapLimit returned ${JSON.stringify(got)}`);
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 2 of 2

A size report that survives a missing file

sizeReport(paths, statFile) returns one line per path, in the order of paths: 'a.txt: 5 bytes' from the file's size, or 'missing.txt: ENOENT' from the error's code when it fails. statFile is stat from node:fs/promises; the tests pass a stand-in to count calls. The starter stats one file at a time and stops at the first error. Start all stats at once and report every file with Promise.allSettled. Run node main.js, then node --test.

This exercise needs Node.js on your computer (the browser version cannot run it). The files and commands are below.

Hints
  1. Hint 1

    paths.map((path) => statFile(path)) starts every stat at once and gives an array of promises.

  2. Hint 2

    await Promise.allSettled(promises) never rejects; each result has status, and value or reason.

  3. Hint 3

    Map each result to a line: result.value.size for 'fulfilled', result.reason.code for 'rejected'. Use the index i to get paths[i].

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import {stat} from 'node:fs/promises';

// One line per path: 'name: N bytes', or 'name: CODE' when stat fails.
export async function sizeReport(paths, statFile = stat) {
  const results = await Promise.allSettled(paths.map((path) => statFile(path)));
  return results.map((result, i) =>
    result.status === 'fulfilled' ? paths[i] + ': ' + result.value.size + ' bytes' : paths[i] + ': ' + result.reason.code
  );
}

console.log(await sizeReport(['a.txt', 'missing.txt', 'b.txt']).catch((error) => error.message));
Run it on your computer

Install Node.js 24 LTS or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

import {stat} from 'node:fs/promises';

// One line per path: 'name: N bytes', or 'name: CODE' when stat fails.
export async function sizeReport(paths, statFile = stat) {
  const lines = [];
  for (const path of paths) {
    const info = await statFile(path);
    lines.push(path + ': ' + info.size + ' bytes');
  }
  return lines;
}

console.log(await sizeReport(['a.txt', 'missing.txt', 'b.txt']).catch((error) => error.message));

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {sizeReport} from './main.js';

test('reports the size of each file', async () => {
  const got = await sizeReport(['a.txt', 'b.txt']);
  assert.deepEqual(got, ['a.txt: 5 bytes', 'b.txt: 3 bytes'], `sizeReport returned ${JSON.stringify(got)}`);
});

test('a missing file is reported with its code, and the others still are', async () => {
  const got = await sizeReport(['a.txt', 'missing.txt', 'b.txt']);
  assert.deepEqual(got, ['a.txt: 5 bytes', 'missing.txt: ENOENT', 'b.txt: 3 bytes'], `sizeReport returned ${JSON.stringify(got)}`);
});

test('every stat starts before the first one finishes', async () => {
  let active = 0;
  let most = 0;
  const fakeStat = async (path) => {
    active++;
    most = Math.max(most, active);
    await new Promise((resolve) => setTimeout(resolve, 10));
    active--;
    return {size: path.length};
  };
  const got = await sizeReport(['x', 'yy', 'zzz', 'w'], fakeStat);
  assert.deepEqual(got, ['x: 1 bytes', 'yy: 2 bytes', 'zzz: 3 bytes', 'w: 1 bytes'], `sizeReport returned ${JSON.stringify(got)}`);
  assert.equal(most, 4, `at most ${most} stats ran at once; expected all 4`);
});

a.txt

hello

b.txt

hi

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Common mistakes

Collecting results inside forEach

import {stat} from 'node:fs/promises';

const sizes = [];
['a.txt', 'b.txt'].forEach(async (name) => {
  sizes.push((await stat(name)).size);
});
console.log(sizes[0].toFixed(0));

What Node.js prints

TypeError: Cannot read properties of undefined (reading 'toFixed')

Why, and the fix

forEach ignores the promises its async callback returns, so nothing has been pushed when the next line runs: sizes[0] is undefined. Make the waiting explicit: const sizes = await Promise.all(names.map(async (name) => (await stat(name)).size)); for parallel reads, or a for...of loop with await for one at a time.

Promise.all when you need every result

import {readFile} from 'node:fs/promises';

const names = ['a.txt', 'missing.txt', 'b.txt'];
const texts = await Promise.all(names.map((name) => readFile(name, 'utf8')));
console.log(texts.length, 'files read');

What Node.js prints

Error: ENOENT: no such file or directory, open 'missing.txt'

Why, and the fix

One missing file rejects the whole Promise.all, and the two texts that were read are lost. If one failure should stop everything, that is right, but catch it. If you want to report every file, use Promise.allSettled and look at each result.status: fulfilled gives value, rejected gives reason.

Passing the promises one by one

const first = Promise.resolve('a');
const second = Promise.resolve('b');

console.log(await Promise.all(first, second));

What Node.js prints

TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))

Why, and the fix

Promise.all, allSettled, any and race take one argument: an iterable, usually an array, of promises. Here Promise.all gets the promise first as its argument, which is not iterable, so it rejects with a TypeError, and second is ignored. Wrap them in an array: await Promise.all([first, second]).

JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

One after another, or all at once

for (const name of names) { texts.push(await readFile(name, 'utf8')); } reads one file, waits for it, then starts the next: in sequence. The total time is the sum of all reads. Use it when a step needs the result of the step before, or when the order of side effects matters. names.map((name) => readFile(name, 'utf8')) starts every read at once and gives an array of promises; await Promise.all(promises) waits for all of them: in parallel. The total is about the slowest read. Promise.all's results are in the order of the input, not the order the reads finish. A forEach with an async callback starts everything and waits for nothing.

When some of them fail

Promise.all rejects as soon as one of its promises rejects, with that first error; you lose the values that did succeed. The other operations are not stopped: they keep running, and their results are thrown away. When you want to report every outcome, use Promise.allSettled. It never rejects; it fulfils with one object per input, {status: "fulfilled", value} or {status: "rejected", reason}, in input order. Two more, briefly: Promise.any fulfils with the first value that succeeds and rejects with an AggregateError only if all of them reject; Promise.race settles like the first promise to settle, success or failure.

At most N at a time, and how to check it

Starting a thousand reads at once can exhaust memory or open files. A pool caps it: start limit workers, and let each loop, taking the next item with const index = next++, until none are left. JavaScript runs one piece of code at a time, so two workers never take the same index. Batches are simpler, await Promise.all over items.slice(i, i + limit), but each batch waits for its slowest item. To check a pool, do not compare wall-clock times, which vary from run to run: count. Increase active before each await and decrease it after, keep the highest value, and compare it with the limit. Compare orders, not milliseconds; if you must time, assert a range.

Sources

Last reviewed October 4, 2026