shtabnoy.com / challenges / promise-all
✦ JS fundamentals

Building Promise.all
from scratch

What happens when you await in a loop vs running promises concurrently? A visual deep-dive into one of JavaScript's most misunderstood patterns.

promisesconcurrencyclosuresasync patterns
// the race

Sequential vs concurrent execution

Watch 5 promises race. The sequential version awaits each one before starting the next. The concurrent version fires them all at once. Click run and see the difference.

Method
Progress
Time
Sequential
Concurrent
[0.00s] → Press "Run the race" to start
// the code

My implementation

No libraries. No cheating. Just the Promise constructor, a counter, and careful index tracking.

promiseAll.ts
export function promiseAll<T>(promises: (T | Promise<T>)[]): Promise<T[]> {
  return new Promise((resolve, reject) => {
    const result: T[] = [];
    let counter = 0;
    const total = promises.length;

    // Edge case: empty array resolves immediately
    if (total === 0) {
      resolve([]);
      return;
    }

    promises.forEach((promise, index) => {
      // Wrap with Promise.resolve to handle non-promise values
      Promise.resolve(promise)
        .then((res) => {
          result[index] = res;  // index preserves order
          counter++;            // counter tracks completion
          if (counter === total) {
            resolve(result);
          }
        })
        .catch((error) => {
          reject(error);  // reject immediately, don't wait
        });
    });
  });
}
💡 Key insight

The index from forEach preserves the original order in the results array, while counter tracks how many promises have completed. These are two different jobs — confusing them is the most common bug in this exercise.

⚠ Common mistake

Using async/await with a for loop creates sequential execution. Each await pauses the entire loop. The fix is to use .then() inside forEach so all promises run concurrently.

// takeaways

What this exercise teaches

Concurrency ≠ parallelism
JavaScript is single-threaded. Promises don't run in parallel — they're scheduled concurrently. The event loop handles them as their async operations complete.
🔒
Closure captures index
Each .then() callback closes over its own index value. This is what preserves order even when promises resolve out of sequence.
💥
Fail-fast rejection
Real Promise.all rejects the moment any input rejects — it doesn't wait for others. This is a design choice. Promise.allSettled takes the opposite approach.
🧩
Edge cases matter
Empty arrays, non-promise values, and the difference between index and counter — these details separate junior from senior implementations.