JavaScript Basics: Combine Tasks with Promise.all and Promise.allSettled
A promise represents an eventual result. When several independent tasks belong together, you can collect their promises and wait for the group. This tutorial compares an all-success result with a report that includes failures.
All examples use built-in features and were tested on Linux with Node.js v24.19.0. Strict assertions check the results and stop execution on a mismatch. No network access or package installation is needed.
Prepare a scratch directory
You need Bash, a text editor, the mktemp, rm, and rmdir utilities, and Node.js on your PATH. Keep using the same terminal throughout. Save each named file as UTF-8. The .mjs extension selects ES modules, which allow the top-level await used here.
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 11. Keep results in input order
Save this as collect-task-results.mjs:
import assert from 'node:assert/strict';
let finishFirstTask;
const firstTask = new Promise((resolve) => {
finishFirstTask = resolve;
});
const secondTask = Promise.resolve('second result');
// The second promise is already fulfilled while the first is pending.
const combined = Promise.all([firstTask, secondTask, 'plain value']);
finishFirstTask('first result');
const results = await combined;
assert.deepEqual(results, ['first result', 'second result', 'plain value']);
assert.deepEqual(await Promise.all([]), []);
console.log(results.join(' | '));
console.log(`Empty input: ${JSON.stringify(await Promise.all([]))}`);Run it:
node collect-task-results.mjsOutput:
first result | second result | plain value
Empty input: []Promise.all() fulfills with values in input order, regardless of completion order. Ordinary values are accepted too. An empty input produces an empty array.
The executor passed to new Promise() runs when that promise is created. Promise.all() collects results; it does not call task functions for you. Pass the promises returned by calling those functions, rather than the functions themselves. For independent asynchronous tasks, create their promises before awaiting the group. This does not make CPU-heavy JavaScript run on multiple threads.
2. Observe rejection without cancelling other work
Save this as observe-group-rejection.mjs:
import assert from 'node:assert/strict';
let finishRemainingTask;
let remainingTaskFinished = false;
const remainingTask = new Promise((resolve) => {
finishRemainingTask = () => {
remainingTaskFinished = true;
resolve('remaining result');
};
});
const failedTask = Promise.reject(new Error('Report unavailable'));
try {
await Promise.all([failedTask, remainingTask]);
assert.fail('Expected the group to reject');
} catch (error) {
assert.equal(error.message, 'Report unavailable');
console.log(`Group failed: ${error.message}`);
}
assert.equal(remainingTaskFinished, false);
finishRemainingTask();
assert.equal(await remainingTask, 'remaining result');
assert.equal(remainingTaskFinished, true);
console.log(`Remaining task finished: ${remainingTaskFinished}`);Run it:
node observe-group-rejection.mjsOutput:
Group failed: Report unavailable
Remaining task finished: trueThe group rejects when an input rejects, without waiting for every task to finish. This does not cancel other operations. Here, the remaining promise can still fulfill after the group fails. Handle the group rejection with try/catch around await; cancellation, if supported by the underlying operation, needs a separate mechanism.
3. Collect successes and failures together
Save this as report-settled-tasks.mjs:
import assert from 'node:assert/strict';
const results = await Promise.allSettled([
Promise.resolve('Profile ready'),
Promise.reject(new Error('Photo missing')),
Promise.resolve('Settings ready'),
]);
assert.deepEqual(results.map((result) => result.status), [
'fulfilled', 'rejected', 'fulfilled',
]);
assert.equal(results[0].value, 'Profile ready');
assert.equal(results[1].reason.message, 'Photo missing');
assert.equal(results[2].value, 'Settings ready');
assert.deepEqual(await Promise.allSettled([]), []);
for (const [index, result] of results.entries()) {
if (result.status === 'fulfilled') {
console.log(`Task ${index + 1}: ${result.value}`);
} else {
console.log(`Task ${index + 1} failed: ${result.reason.message}`);
}
}Run it:
node report-settled-tasks.mjsOutput:
Task 1: Profile ready
Task 2 failed: Photo missing
Task 3: Settings readyPromise.allSettled() waits until every input has either fulfilled or rejected. Its result records remain in input order. Check status before reading value or reason: fulfilled records have value, while rejected records have reason.
Use this form when a partial result is useful, such as a batch report. A failed input becomes a rejected record rather than rejecting the group. Inspect those records so failures do not disappear silently. Rejection reasons can be any JavaScript value; this example deliberately uses Error objects, so reading .message is appropriate here.
Neither method adds a timeout or limits how many tasks run at once. In particular, allSettled() keeps waiting if an input never settles. For large batches, choose a separate concurrency limit instead of starting every operation at once.
Clean up
In the same terminal, remove only these example files, return to your original directory, and remove the empty scratch directory:
rm -- collect-task-results.mjs observe-group-rejection.mjs report-settled-tasks.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirAcknowledgements
This post was created with assistance from AI.