DEV Community

Cover image for Which promise method do you need?
Manuj Sankrit
Manuj Sankrit

Posted on

Which promise method do you need?

Last week, a flatmate of mine — a seasoned Java/Spring backend developer — knocked on my door with a quick frontend question. He'd been handed a small task on the frontend side of his project, nothing major, and had reached out for a sanity check before pushing his code.

He had used Promise.race to implement a fallback — if the primary API failed, he wanted to use the backup. The logic made sense in his head. But the implementation was wrong, and the bug was subtle enough that it had slipped through his initial testing.

When I asked him "do you know the difference between race and any?", he paused. And honestly? The pause made me think. Because two years ago, I would have paused too.

That conversation sent me down a rabbit hole — rebuilding polyfills for all four Promise methods — all, any, race, and allSettled — from scratch, just to make sure I truly understood the internals and not just the surface-level API.

Now, there are already separate blog posts floating around on each of these (including some I wrote a couple of years ago 😄). But the real gap isn't understanding each method individually — it's knowing how they relate to each other and which one to reach for in a given situation.

So this is that blog where I wanted to share my learning. The consolidated one. The "one ring to rule them all" of Promise methods. 💍


The Quick Cheat Sheet (For the Impatient 😄)

Method Resolves when Rejects when Returns
Promise.all All resolve Any one rejects Array of values (ordered)
Promise.any Any one resolves All reject First resolved value
Promise.race Any one settles Any one rejects first First settled value
Promise.allSettled All settle (always resolves) Never Array of {status, value/reason}

The Decision Framework

Before the deep dive, here's the mental model I use when picking between these four:

Do you need ALL to succeed?Promise.all

Do you need just ONE to succeed, ignoring the rest?Promise.any

Do you want whoever finishes first — win or lose?Promise.race

Do you want the full picture regardless of success or failure?Promise.allSettled

If that's all you needed, you're welcome. 😄 For everyone else, let's go deeper.


Promise.all

What it does

Takes an array of promises and returns a single promise that resolves with an array of all resolved values, in the same order as the input. If any one promise rejects, the whole thing rejects immediately — it doesn't wait for the rest.

const promise1 = Promise.resolve('Hello');
const promise2 = Promise.resolve(42);
const promise3 = new Promise((resolve) => setTimeout(resolve, 100, 'World'));

Promise.all([promise1, promise2, promise3])
  .then((values) => console.log(values))
  .catch((error) => console.error(error));
// Output: ["Hello", 42, "World"]
Enter fullscreen mode Exit fullscreen mode

When to use it

  • Fetching data from multiple APIs where all results are required to render (think dashboards)
  • Uploading multiple files and waiting for all to complete before showing a success message
  • Batch database operations where all must succeed together

Polyfill

function all(promises) {
  function executorFunction(resolve, reject) {
    const result = [];
    let pendingCount = promises.length;

    if (pendingCount === 0) {
      resolve(result);
      return;
    }

    promises.forEach((promise, index) => {
      Promise.resolve(promise).then((value) => {
        result[index] = value; // 👈 index assignment, not push
        if (--pendingCount === 0) {
          resolve(result);
        }
      }, reject); // first rejection short-circuits everything
    });
  }
  return new Promise(executorFunction);
}
Enter fullscreen mode Exit fullscreen mode

⚠️ The gotcha hiding in plain sight: Notice I use result[index] = value and NOT result.push(value). If I used push, the order would depend on which promise resolved first — not the original input order. Promise.all guarantees output order matches input order, and this one line is why. The pendingCount counter is how I know when every promise has settled without needing await.


Promise.any

What it does

Returns a promise that resolves with the first fulfilled value. Rejections are silently ignored — unless all promises reject, in which case it throws an AggregateError wrapping all the rejection reasons.

const promise1 = Promise.reject(new Error('First failed'));
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'Success'));
const promise3 = Promise.reject(new Error('Second failed'));

Promise.any([promise1, promise2, promise3])
  .then((value) => console.log(value))
  .catch((error) => console.error(error));
// Output: "Success"
Enter fullscreen mode Exit fullscreen mode

When to use it

  • Trying multiple CDN sources and using whichever responds first
  • Primary + backup service pattern — use the backup only if primary fails
  • Optimistic UI — show cached data or fresh API data, whoever arrives first wins

Polyfill

function any(promises) {
  function executorFunction(resolve, reject) {
    const errors = [];
    let pendingCount = promises.length;
    if (pendingCount === 0) {
      reject(new AggregateError(errors, 'No promises were passed'));
      return;
    }

    promises.forEach((promise, idx) => {
      Promise.resolve(promise)
        .then((val) => resolve(val)) // first resolve wins
        .catch((err) => {
          errors[idx] = err; // preserve order of errors
          if (--pendingCount === 0) {
            reject(new AggregateError(errors, 'All promises were rejected'));
          }
        });
    });
  }
  return new Promise(executorFunction);
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Two gotchas here:

Gotcha 1 — AggregateError is not a regular Error: When all promises reject, the catch block receives an AggregateError. Don't try to read error.message expecting individual reasons — access error.errors (the array) to get each rejection reason.

Gotcha 2 — Empty array behavior: Promise.any([]) immediately rejects with AggregateError. This is the opposite of Promise.all([]), which immediately resolves with an empty array. Catch this during interviews — it's a classic trap. 😄

Notice the symmetry with the Promise.all polyfill — but inverted. In all, a single rejection short-circuits. In any, a single resolution short-circuits. We still use indexed assignment for errors (not push) so the error order matches the input order, which matters when you're debugging why everything blew up.


Promise.race

What it does

Returns a promise that settles as soon as the first promise settles — resolve or reject. Whoever finishes first, wins. The others are abandoned.

const promise1 = new Promise((resolve) => setTimeout(resolve, 200, 'Slow'));
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'Fast'));

Promise.race([promise1, promise2])
  .then((value) => console.log(value))
  .catch((error) => console.error(error));
// Output: "Fast"
Enter fullscreen mode Exit fullscreen mode

When to use it

The classic use case — implementing a timeout for any async operation:

const fetchData = () => fetch('https://api.example.com/data');

const timeout = (ms) =>
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Request timed out')), ms),
  );

Promise.race([fetchData(), timeout(5000)])
  .then((response) => response.json())
  .catch((error) => console.error(error));
// Rejects with "Request timed out" if API doesn't respond in 5 seconds
Enter fullscreen mode Exit fullscreen mode

Polyfill

function race(promises) {
  function executorFunction(resolve, reject) {
    for (const promise of promises) {
      Promise.resolve(promise).then(resolve, reject);
    }
  }
  return new Promise(executorFunction);
}
Enter fullscreen mode Exit fullscreen mode

This is the simplest polyfill of the four — and the elegance is worth appreciating. Once a Promise settles, subsequent calls to resolve or reject are silently ignored by the JS engine. So we just attach handlers to every promise and let the first one win naturally. No counter, no result array, no complexity.

⚠️ The race vs any confusion: This is the most common mixup I see.

  • race = first to finish (resolve OR reject). A rejection wins the race too.
  • any = first to succeed. Rejections are ignored until all fail.

If the first promise to settle in Promise.race is a rejection, the whole thing rejects — it doesn't wait for a successful one. If that's not what you want, you probably want Promise.any.

⚠️ Empty array is a silent trap: Promise.race([]) returns a promise that is permanently pending — it never settles. Unlike Promise.all([]) which resolves immediately, or Promise.any([]) which rejects immediately, race just waits forever. Always guard against this in production code:

if (promises.length === 0)
  return Promise.reject(new Error('No promises provided'));

Promise.allSettled

What it does

Waits for all promises to settle and returns an array of objects describing each outcome. It never rejects — you always get the full picture, wins and losses both.

const promise1 = Promise.resolve('Hello');
const promise2 = Promise.reject(new Error('Something went wrong'));
const promise3 = new Promise((resolve) => setTimeout(resolve, 100, 'World'));

Promise.allSettled([promise1, promise2, promise3]).then((results) =>
  console.log(results),
);
// Output:
// [
//   { status: "fulfilled", value: "Hello" },
//   { status: "rejected", reason: Error: Something went wrong },
//   { status: "fulfilled", value: "World" }
// ]
Enter fullscreen mode Exit fullscreen mode

When to use it

  • Fetching from multiple APIs where partial success is acceptable — show what you have, log what failed
  • Batch processing where some failures are expected and shouldn't stop the rest
  • Running multiple independent operations and reporting the full outcome

Polyfill

function allSettled(promises) {
  function executorFunction(resolve) {
    // ☝️ no reject parameter — we always resolve
    const result = [];
    let pendingCount = promises.length;

    if (pendingCount === 0) {
      resolve(result);
      return;
    }

    promises.forEach((promise, idx) => {
      Promise.resolve(promise)
        .then((value) => (result[idx] = { status: 'fulfilled', value }))
        .catch((reason) => (result[idx] = { status: 'rejected', reason }))
        .finally(() => {
          if (--pendingCount === 0) {
            resolve(result);
          }
        });
    });
  }
  return new Promise(executorFunction);
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Implementation insight: The executor function deliberately omits the reject parameter — this promise should never reject, so why even have it there? The .finally() is the cleanest way to decrement the counter regardless of whether .then or .catch ran. Both write to result[idx] before .finally fires, so by the time we call resolve, every slot is already filled.


The Side-by-Side That Makes It Click

Let me pass the same promises to all four and you'll immediately see the difference:

const fast = Promise.resolve('fast');
const slow = new Promise((res) => setTimeout(res, 200, 'slow'));
const fail = Promise.reject(new Error('failed'));

// Promise.all — one failure, everything fails
Promise.all([fast, slow, fail]);
// ❌ Rejects with Error: "failed"

// Promise.any — one success is enough
Promise.any([fast, slow, fail]);
// ✅ Resolves with "fast"

// Promise.race — fail settles first (it's already rejected)
Promise.race([fail, fast, slow]);
// ❌ Rejects with Error: "failed"

// Promise.allSettled — never rejects, full picture
Promise.allSettled([fast, slow, fail]);
// ✅ Resolves with:
// [
//   { status: 'fulfilled', value: 'fast' },
//   { status: 'fulfilled', value: 'slow' },
//   { status: 'rejected', reason: Error: 'failed' }
// ]
Enter fullscreen mode Exit fullscreen mode

Common Mistakes (That I've Also Made 😅)

1. Reaching for Promise.all when partial failure is fine

If your dashboard can render with 3 out of 4 APIs responding, use allSettled and handle failures individually. Promise.all will discard all results the moment one fails. Most real-world scenarios are more forgiving than Promise.all assumes.

2. Confusing race and any

race = first to finish. any = first to succeed. If you want a timeout that only triggers when your request actually fails (not just when it's slower than a competing promise), you want any not race.

3. Forgetting AggregateError in Promise.any

Your catch block for Promise.any will receive an AggregateError when all promises reject. Accessing error.message won't give you the individual reasons. You need error.errors — the array of all rejection reasons.

4. Using push instead of index assignment in custom implementations

If you ever write a custom aggregator and use result.push(value) instead of result[index] = value, you'll silently break the order guarantee. The output order will depend on resolution speed, not input order. And this will only show up in production under load. Fun times. 😄


Quick Reference

Method Use when...
Promise.all You need ALL results and one failure should abort everything
Promise.any You need ONE success and don't care which one
Promise.race You want the first to settle — success or failure
Promise.allSettled You want all outcomes and will handle each individually

Building these polyfills from scratch was genuinely one of the better learning exercises I've done recently. Reading docs tells you what these methods do. Writing the internals tells you why they behave the way they do — and that's where the real understanding lives.

Until next time, Pranipat 🙏!

Top comments (0)