It fits independent operations when every result is required, but its ordering and early rejection rules create common interview traps.
Promise.allaccepts an iterable and returns a promise that fulfills with input-ordered values after every input fulfills, or rejects when it observes an input rejection.
What Promise.all Does
The method accepts an iterable, such as an array or Set. It returns a promise with two possible outcomes:
- It fulfills with an array after every input fulfills.
- It rejects when the aggregate observes an input rejection.
The fulfillment array follows the iterable's input order. Completion order does not rearrange it.
A common use is requesting independent resources and destructuring the combined result:
async function fetchJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error('A request failed');
}
return response.json();
}
async function loadInterviewPage() {
const [question, attempts] = await Promise.all([
fetchJson('/api/question'),
fetchJson('/api/attempts'),
]);
return { question, attempts };
}
The two fetch calls happen before Promise.all receives their promises. The combinator then waits for both. The same distinction matters when discussing Promise.all as a coding question: it coordinates existing inputs, but it does not call the functions that produce them.
Each fetchJson call validates and parses its response independently, so parsing one response can overlap the other request. Both results are required, so Promise.all matches the requirement.
How Promise.all Preserves Order
Input order and completion order are different concepts. Promise.all records the position of each input and stores its fulfillment value at that position.
This example deliberately makes the second promise finish first:
function delay(value, milliseconds) {
return new Promise((resolve) => {
setTimeout(() => resolve(value), milliseconds);
});
}
Promise.all([
delay('first', 30),
delay('second', 5),
]).then((values) => {
console.log(JSON.stringify(values));
});
["first","second"]
The value "second" becomes available earlier, but it still belongs at index 1. The aggregate cannot fulfill until both positions have values.
A useful interview model has three parts:
- Assign an index to each input.
- Store each fulfillment value at its assigned index.
- Count how many inputs have not yet recorded a fulfillment value.
When the remaining count reaches zero, the implementation resolves with the stored array. This model explains why pushing values as promises finish is wrong. results.push(value) records completion order instead of input order.
Each input is normalized through the promise constructor's resolve operation. For native Promise.all, that means promises, thenables, and plain values can appear together. A thenable is an object with a callable then property.
const thenable = {
then(resolve) {
resolve('from thenable');
},
};
Promise.all([7, Promise.resolve(8), thenable]).then((values) => {
console.log(JSON.stringify(values));
});
[7,8,"from thenable"]
An empty iterable returns an already fulfilled promise whose value is an empty array. A nonempty iterable with no pending promises still fulfills asynchronously.
console.log('before');
Promise.all([1, 2]).then((values) => {
console.log(JSON.stringify(values));
});
console.log('after');
before
after
[1,2]
The state of the returned promise and the time at which a reaction runs are separate concerns. This distinction prevents an incorrect implementation from calling user code synchronously.
Concurrency, Parallelism, and Sequential Await
Concurrency means multiple operations can make progress during overlapping periods. Parallelism means work executes at the same instant. Promise.all does not guarantee parallel execution, and the promise objects themselves do not perform the underlying work.
For native promises, the producing function normally starts or schedules the relevant operation when it is called; Promise.all does not call that function. Normalizing a thenable can access and invoke its then, however, so a lazy thenable may start work at that point. In this sequential version, the second call does not happen until the first result arrives:
async function loadSequentially() {
const question = await fetchQuestion();
const attempts = await fetchAttempts();
return { question, attempts };
}
If the operations are independent, call both functions before waiting for their combined result:
async function loadConcurrently() {
const questionPromise = fetchQuestion();
const attemptsPromise = fetchAttempts();
const [question, attempts] = await Promise.all([
questionPromise,
attemptsPromise,
]);
return { question, attempts };
}
The shorter form has the same call order:
async function loadConcurrently() {
const [question, attempts] = await Promise.all([
fetchQuestion(),
fetchAttempts(),
]);
return { question, attempts };
}
This arrangement can reduce total waiting time when independent operations can overlap. It does not promise a universal improvement. Browsers, networks, servers, and resource pools impose their own constraints.
Dependencies change the correct structure. If fetchAttempts needs an identifier returned by fetchQuestion, awaiting the first result is required:
async function loadDependentData() {
const question = await fetchQuestion();
const attempts = await fetchAttempts(question.id);
return { question, attempts };
}
Another common mistake is passing function references:
const tasks = [fetchQuestion, fetchAttempts];
const results = await Promise.all(tasks);
This resolves with the function objects because Promise.all does not invoke them. Call each function first:
const tasks = [fetchQuestion, fetchAttempts];
const results = await Promise.all(tasks.map((task) => task()));
Array mapping is useful when each item creates one independent operation:
const attempts = await Promise.all(
candidateIds.map((candidateId) => fetchAttempts(candidateId))
);
For interview practice that moves beyond one aggregate, an async task queue provides a useful follow-up problem.
Errors, Cancellation, and Concurrency Limits
Promise.all is fail-fast. Here, fail-fast means the aggregate rejects after observing an input rejection instead of waiting to produce a complete array of successful values.
Use try/catch around await when the surrounding function should handle the failure:
async function loadDashboard() {
try {
const [question, attempts] = await Promise.all([
fetchQuestion(),
fetchAttempts(),
]);
return { question, attempts };
} catch (error) {
console.error('Dashboard loading failed:', error);
throw error;
}
}
The rejection reason comes from the first rejection observed by the aggregate. That is not necessarily the first item in input order.
Fail-fast does not mean cancel-fast. Rejecting the aggregate does not itself stop work that has already started. Another request, timer, or database operation can continue after the combined promise has rejected.
Some APIs provide a separate cancellation mechanism. For browser fetch, an AbortController can connect multiple requests to one signal:
async function loadWithAbort() {
const controller = new AbortController();
try {
return await Promise.all([
fetch('/api/question', { signal: controller.signal }),
fetch('/api/attempts', { signal: controller.signal }),
]);
} catch (error) {
controller.abort();
throw error;
}
}
Calling abort() can abort connected fetch requests, response-body consumption, and streams. Cancellation remains an explicit decision made through the operation's API. Promise.all does not add cancellation to an operation that lacks it.
Promise.all is also not a concurrency limiter. Mapping a large collection creates every operation before the combinator waits:
const results = await Promise.all(
candidateIds.map((candidateId) => fetchAttempts(candidateId))
);
If only a bounded number should be active, use batching or a worker-pool pattern. A Promise Pool exercise isolates that requirement. The pool decides when to call each producing function. Promise.all can still coordinate one selected batch, but it does not choose the batch size.
Promise.all vs. Other Promise Combinators
The correct combinator depends on which outcome the caller needs.
| Combinator | Successful result | Rejection behavior | Empty iterable |
|---|---|---|---|
Promise.all | All fulfillment values in input order | Rejects after observing an input rejection | Fulfills with [] |
Promise.allSettled | Every outcome object in input order | Does not reject because an input rejects | Fulfills with [] |
Promise.any | First fulfillment value | Rejects with AggregateError when every input rejects | Rejects with AggregateError |
Promise.race | State and value or reason of the first settlement | Rejects if the first settlement is a rejection | Remains pending |
Use Promise.all when every operation must succeed before the combined result is useful. Loading a question and its required test cases fits this rule.
Use Promise.allSettled when every outcome matters. A results screen that reports which optional resources loaded and which failed may need all status objects.
Use Promise.any when one successful result is enough. Rejections are tolerated until every input has rejected.
Use Promise.race when the first settlement decides the outcome. That settlement can be either fulfillment or rejection.
These methods do not cancel losing or unfinished operations. If cancellation is required, it must be designed separately.
Implement Promise.all in an Interview
An interview implementation should begin with a behavioral contract:
- Accept a synchronous iterable.
- Preserve input order.
- Normalize each input with
Promise.resolve. - Fulfill after every input fulfills.
- Fulfill an empty input with
[]. - Reject with an observed rejection reason.
Before reading the reference solution, implement the contract above in this starter function:
function interviewPromiseAll(iterable) {
// Implement the contract above.
}
Run your implementation against the test matrix in the reference section when you are ready to compare it.
Reference solution and runnable test matrix
The implementation below is the canonical version for this exercise. Its test matrix is included so the code runs as one self-contained program.
function interviewPromiseAll(iterable) {
return new Promise((resolve, reject) => {
if (
iterable == null ||
typeof iterable[Symbol.iterator] !== 'function'
) {
throw new TypeError('Expected a synchronous iterable');
}
const inputs = Array.from(iterable);
const results = new Array(inputs.length);
if (inputs.length === 0) {
resolve([]);
return;
}
let remaining = inputs.length;
inputs.forEach((input, index) => {
Promise.resolve(input).then(
(value) => {
results[index] = value;
remaining -= 1;
if (remaining === 0) {
resolve(results);
}
},
reject
);
});
});
}
function delay(value, milliseconds, shouldReject = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldReject) {
reject(new Error(value));
} else {
resolve(value);
}
}, milliseconds);
});
}
async function runTests() {
const ordered = await interviewPromiseAll([
delay('first', 30),
delay('second', 5),
]);
console.log(`ordering: ${JSON.stringify(ordered)}`);
const thenable = {
then(resolve) {
resolve('thenable');
},
};
const mixed = await interviewPromiseAll([
3,
Promise.resolve(4),
thenable,
]);
console.log(`mixed: ${JSON.stringify(mixed)}`);
const empty = await interviewPromiseAll([]);
console.log(`empty: ${JSON.stringify(empty)}`);
let slowSettled = false;
const slow = delay('slow success', 30).then((value) => {
slowSettled = true;
return value;
});
try {
await interviewPromiseAll([
slow,
delay('early rejection', 5, true),
]);
} catch (error) {
console.log(`rejection: ${error.message}`);
console.log(`slow pending at rejection: ${!slowSettled}`);
}
await slow;
console.log(`slow continued: ${slowSettled}`);
}
runTests();
ordering: ["first","second"]
mixed: [3,4,"thenable"]
empty: []
rejection: early rejection
slow pending at rejection: true
slow continued: true
The index captured by each callback is the ordering invariant. The remaining counter tracks inputs whose fulfillment has not yet been recorded. Each fulfillment writes exactly one position and reduces the counter once.
Promise.resolve(input) is necessary. Without it, plain values have no .then method, and thenables would not participate through normal promise assimilation.
The rejection handler forwards the observed reason to the outer promise. Later settlements cannot change the outer promise after it rejects.
This remains an interview-scoped implementation, not a replacement for the native method. Native Promise.all has generic constructor behavior, detailed iterator handling and closing rules, thenable safeguards, and semantics for promise subclass constructors. The explicit iterator check limits this exercise to synchronous iterables before Array.from collects their values, but the code does not recreate the full specification machinery.
Edge Cases and Follow-Up Questions
The runnable matrix above covers the cases most likely to expose a broken implementation.
The ordering test proves that fulfillment values use input indexes. The mixed-input test covers a plain value, an existing promise, and a thenable. The empty-input test checks that the fulfillment value is []. The rejection test verifies that the aggregate rejects while the slow input is still pending, then shows that the slow operation continues.
Common broken implementations have recognizable causes:
- Pushing values into the result array records completion order.
- Calling
.thendirectly on each input fails for plain values. - Forgetting the empty case leaves the returned promise pending.
- Passing async function references returns those functions instead of calling them.
- Awaiting each operation before building the array removes the intended overlap.
- Treating aggregate rejection as cancellation misstates what happened to unfinished work.
For n inputs, this interview implementation uses an array of n results and attaches handling to each input. Converting the iterable and visiting its values takes linear work. The stored result array uses linear space.
A concurrency-limit follow-up changes the problem. Instead of creating every promise immediately, a bounded worker pool keeps a fixed set of workers active. Each worker takes the next producing function only after its current operation settles. The final results can still be stored by original index.
A concise spoken explanation can follow four points:
- State the contract: all fulfillments are required, results preserve input order, and an observed rejection rejects the aggregate.
- Name the invariants: one result slot per input and one remaining counter.
- Explain normalization:
Promise.resolvehandles promises, thenables, and plain values. - State the production caveats: the combinator does not call promise-producing functions, cancel unfinished work, or limit concurrency, though assimilating a lazy thenable may start its work.
For more timed exercises and worked solutions, UIReady Premium Lifetime can extend this implementation into a full interview practice session.