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.