Label Promise.any() inputs and classify AggregateError.errors for actionable retries

Label Promise.any() inputs and classify AggregateError.errors for actionable retries

calendar_today ago • schedule2 min read
— Originally published at chriseugenerodriguez.com

Prerequisites: familiarity with Promise.any(), AggregateError, and a simple retry vs fatal policy for network/service errors.

Implementation decision: wrap each candidate source with a label before calling Promise.any(). Convert rejections into Error objects that include a stable sourceLabel and preserve originalReason. Call Promise.any() over the wrapped promises. If it rejects with an AggregateError, iterate aggregate.errors in the same input order and run a predicate per-entry that determines whether the failure is retryable or should be treated as fatal. Keep the label and the reason paired so downstream code can act against a single source.

Expected result: when any source fulfills, you get that value. When every source rejects, you get an AggregateError whose errors array contains the labeled rejection objects in the order the sources were provided. The classification step returns two lists (retryable and fatal) that preserve input order so the caller can retry specific sources or remove them from the decision set.

One limitation: this approach does not cancel in-flight work after a decision. Promise.any() does not stop other promises from continuing; you must implement cleanup or cancellation at the source. Also remember the errors array is not chronological — it is positional — so add timestamps or logging if you need a timeline.

Working example

export function raceAny(labeledPromises) {
  // labeledPromises: Array of { label: string, promise: Promise }
  // Returns a Promise that behaves like Promise.any but preserves labels
  // on per-source rejections by rethrowing labeled Error objects.
  if (!Array.isArray(labeledPromises)) {
    throw new TypeError('expected an array of {label,promise}');
  }

  const wrapped = labeledPromises.map(({ label, promise }) => {
    if (typeof label !== 'string') throw new TypeError('label must be a string');
    // Ensure we treat non-Promise values as fulfilled promises (like Promise.any does)
    const p = Promise.resolve(promise);
    return p.catch((reason) => {
      // Wrap rejection so AggregateError.errors has an inspectable object.
      // Keep the original reason on `.originalReason` for callers that need it.
      const err = new Error(String(reason === undefined ? 'undefined' : reason));
      // stable label and original reason preserved as properties (non-enumerable like typical Error props)
      Object.defineProperty(err, 'sourceLabel', { value: label, enumerable: true });
      Object.defineProperty(err, 'originalReason', { value: reason, enumerable: true });
      // rethrow so Promise.any receives this labeled Error as the rejection reason
      throw err;
    });
  });

  return Promise.any(wrapped);
}

export function classifyAggregateError(aggregateError, isRetryable) {
  // Given an AggregateError from Promise.any when all inputs reject,
  // return { retryable: [...], fatal: [...] } arrays containing objects
  // { label, reason } in the same order as aggregateError.errors.
  if (!(aggregateError instanceof AggregateError)) {
    throw new TypeError('expected an AggregateError');
  }
  if (typeof isRetryable !== 'function') {
    throw new TypeError('isRetryable must be a function');
  }

  const result = { retryable: [], fatal: [] };
  for (const err of aggregateError.errors) {
    // We expect the wrapper in raceAny to attach sourceLabel and originalReason.
    const label = err && typeof err.sourceLabel === 'string' ? err.sourceLabel : '<unknown>';
    const reason = err && Object.prototype.hasOwnProperty.call(err, 'originalReason') ? err.originalReason : err;
    const entry = { label, reason };
    try {
      if (isRetryable(entry)) result.retryable.push(entry);
      else result.fatal.push(entry);
    } catch (_e) {
      // If classifier throws, treat as fatal to be conservative
      result.fatal.push(entry);
    }
  }
  return result;
}

Read the full article: https://chriseugenerodriguez.com/blog/promise-any-aggregateerror-actionable-failures

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelski - Mar 19

Frameworks Are Institutional Memory

Ken W. Algerverified - Sep 17

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelski - Apr 23

Your Tech Stack Isn’t Your Ceiling. Your Story Is

Karol Modelski - Apr 9

A Message From Our Founder: On the Winding Down of LaaSy™

Ken W. Algerverified - Oct 29
chevron_left
546 Points • 24 Badges
Santa Monica, CA • chriseugenerodriguez.com
19Posts
0Comments
4Connections
10 years fe

Related Jobs

View all jobs →

Commenters (This Week)

3 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!