Promise combinators: all, allSettled, race, any

Intermedio
JavaScript
JavaScript
Actualizado: 04/05/2026

Diagrama: Promise combinators

Qué son los Promise combinators

Cuando una aplicación lanza varias operaciones asíncronas en paralelo (peticiones HTTP, lecturas de disco, procesos en segundo plano), la pregunta relevante no es "¿cómo hago esta en concreto?" sino "¿cómo espero a varias a la vez?". Promise ofrece cuatro combinators estáticos para resolverlo, cada uno con una semántica diferente.

| Combinator | Resuelve cuando | Rechaza cuando | Resultado | |------------|-----------------|----------------|-----------| | Promise.all | Todas se cumplen | Alguna rechaza (fail-fast) | Array de valores en orden | | Promise.allSettled | Todas terminan (cumplidas o rechazadas) | Nunca | Array de {status, value\|reason} | | Promise.race | Primera en terminar (lo que sea) | Primera si rechaza | Valor/razón de la primera | | Promise.any | Primera en cumplirse | Todas rechazan | Valor de la primera cumplida, o AggregateError |

Saber cuál usar en cada caso es clave para evitar esperas innecesarias, errores enmascarados o, al revés, fallos globales cuando en realidad el caso concreto sí tolera que alguna operación falle.

Promise.all: todas o nada

Promise.all(iterable) recibe un iterable de promesas y devuelve otra promesa que se cumple con un array de resultados si todas se cumplen, o se rechaza en cuanto una falla, descartando las demás.

const userP   = fetch("/api/user").then(r => r.json());
const postsP  = fetch("/api/posts").then(r => r.json());
const likesP  = fetch("/api/likes").then(r => r.json());

const [user, posts, likes] = await Promise.all([userP, postsP, likesP]);

Aunque las tres peticiones corren en paralelo, los resultados llegan en el mismo orden que las promesas originales, independientemente de cuál termine antes. Esto hace que Promise.all sea perfecto cuando todas las operaciones son necesarias para continuar y no tiene sentido seguir si una falla.

Comportamiento ante errores

Si postsP rechaza, Promise.all rechaza inmediatamente con ese error. userP y likesP siguen ejecutándose, pero su resultado se ignora:

try {
  await Promise.all([userP, postsP, likesP]);
} catch (err) {
  console.error("Al menos una petición falló:", err);
}

Este comportamiento "fail-fast" suele ser deseable cuando la pantalla o la operación no tienen sentido sin todos los datos.

Iterable vacío

Con un array vacío, Promise.all se resuelve de inmediato con []:

console.log(await Promise.all([])); // []

Es un caso borde útil de recordar cuando el array se construye dinámicamente.

Promise.allSettled: recoger todos los resultados

Introducido en ES2020, Promise.allSettled espera a que todas las promesas terminen, sean cumplidas o rechazadas, y devuelve un array con el resultado de cada una:

const results = await Promise.allSettled([
  fetch("/api/primary").then(r => r.json()),
  fetch("/api/secondary").then(r => r.json()),
  fetch("/api/broken").then(r => r.json())
]);

for (const r of results) {
  if (r.status === "fulfilled") {
    console.log("OK:", r.value);
  } else {
    console.warn("FAIL:", r.reason);
  }
}

Cada entrada es { status: "fulfilled", value } o { status: "rejected", reason }. Promise.allSettled nunca rechaza: la promesa resultante siempre se cumple una vez que todas las entradas terminan.

Es la elección adecuada cuando las operaciones son independientes y el fallo parcial no invalida el resultado global. Escenarios típicos:

  • Sincronizar datos con varias APIs y construir un informe con cuáles han ido bien.
  • Enviar varias notificaciones en paralelo y reintentar solo las fallidas.
  • Precargar recursos opcionales.

Ejemplo de conteo:

function summarize(results) {
  return {
    total: results.length,
    ok: results.filter(r => r.status === "fulfilled").length,
    fail: results.filter(r => r.status === "rejected").length
  };
}

Promise.race: la primera en ocurrir gana

Promise.race(iterable) devuelve una promesa que se cumple o rechaza con el primer resultado del iterable, sea cual sea. No discrimina entre éxito y fracaso: si la primera en terminar rechaza, race rechaza.

const result = await Promise.race([
  fetch("/api/fast"),
  fetch("/api/slow")
]);

Implementar un timeout

El uso más habitual es añadir un timeout a una operación arbitraria. Si la operación tarda más de ms milisegundos, la promesa de timeout rechaza primero:

function timeout(ms, label = "timeout") {
  return new Promise((_, reject) => {
    setTimeout(() => reject(new Error(label)), ms);
  });
}

async function fetchWithTimeout(url, ms = 5000) {
  return Promise.race([
    fetch(url).then(r => r.json()),
    timeout(ms, `request to ${url} exceeded ${ms}ms`)
  ]);
}

try {
  const data = await fetchWithTimeout("/api/slow", 2000);
  console.log(data);
} catch (err) {
  console.error(err.message);
}

Nota: Promise.race no cancela las otras promesas; simplemente deja de esperarlas. Si la operación consume recursos que conviene liberar, usa AbortController (lo veremos en otra lección).

Iterable vacío

Con un array vacío, Promise.race nunca se resuelve. Es un caso a evitar: asegúrate siempre de pasar al menos una promesa.

Promise.any: la primera exitosa

Promise.any (ES2021) es hermano de race, pero solo toma en cuenta las promesas cumplidas. Si todas rechazan, devuelve un AggregateError que agrupa todas las razones en su propiedad errors.

try {
  const fastest = await Promise.any([
    fetch("https://mirror1.example/data"),
    fetch("https://mirror2.example/data"),
    fetch("https://mirror3.example/data")
  ]);
  console.log("Respondió primero:", fastest.url);
} catch (err) {
  if (err instanceof AggregateError) {
    console.error("Todas fallaron:", err.errors);
  }
}

Es el combinator ideal cuando hay varias alternativas y basta con que alguna funcione: mirrors, fallbacks, réplicas. Si solo quieres el primer éxito sin perder tiempo esperando a los demás, Promise.any es más adecuado que Promise.race, porque los rechazos intermedios se ignoran.

Tabla comparativa y cuándo usar cada uno

| Escenario | Combinator | |-----------|------------| | Necesito todos los resultados, no tiene sentido seguir si alguno falla | Promise.all | | Quiero todos los resultados y manejar fallos parciales | Promise.allSettled | | Quiero el primero que termine (éxito o fallo), para implementar un timeout | Promise.race | | Quiero el primero que funcione, ignorando fallos individuales | Promise.any |

Acelerar operaciones: siempre en paralelo

Un error frecuente al traducir código secuencial es encadenar await cuando las operaciones son independientes:

// Secuencial: tarda user + posts + likes
const user  = await fetchUser();
const posts = await fetchPosts();
const likes = await fetchLikes();

// Paralelo con Promise.all: tarda el máximo de las tres
const [user2, posts2, likes2] = await Promise.all([
  fetchUser(), fetchPosts(), fetchLikes()
]);

Este es el uso por defecto de Promise.all: acelerar operaciones que no dependen entre sí.

Evitar "unhandled promise rejection"

Con combinators es importante manejar errores en la promesa combinada. Si capturas los errores dentro de cada promesa individual y no en el combinador, puede que tu lógica no se entere. Con allSettled el problema desaparece, pero con all y race hay que envolver en try/catch o encadenar .catch.

Patrón: concurrencia limitada

Ninguno de los combinators limita la concurrencia: lanzar Promise.all con 10 000 elementos provoca 10 000 operaciones en paralelo. Una utilidad habitual es ejecutar en bloques:

async function mapLimit(items, limit, fn) {
  const results = new Array(items.length);
  let index = 0;

  const workers = Array.from({ length: limit }, async () => {
    while (index < items.length) {
      const current = index++;
      results[current] = await fn(items[current], current);
    }
  });

  await Promise.all(workers);
  return results;
}

Se lanzan limit workers; cada uno toma el siguiente índice hasta agotar el array. Es el equivalente práctico a un Promise.all con concurrencia limitada.

Buenas prácticas

  • Usa Promise.all como opción por defecto para paralelizar operaciones independientes.
  • Para escenarios con fallos tolerables, prefiere Promise.allSettled antes que envolver cada promesa con su propio catch: el resultado final es más fácil de procesar.
  • Usa Promise.race solo cuando necesites la primera respuesta o un timeout; recuerda que las otras promesas siguen ejecutándose.
  • Promise.any es la forma idiomática de implementar failover: acepta varias alternativas y devuelve la primera que funcione.
  • Para operaciones muy numerosas, añade concurrencia limitada: lanzarlas todas de golpe puede saturar la red o la base de datos.
  • Siempre captura errores del combinator, no solo de las promesas individuales.

Resumen

Los cuatro combinators (all, allSettled, race, any) cubren las estrategias clásicas para coordinar varias promesas en paralelo: todas-o-nada, todas-tolerando-fallos, la primera que ocurra y la primera que funcione. Elegir el adecuado hace que el código asíncrono refleje con precisión la intención del negocio y evita tanto las esperas innecesarias como los errores silenciados. Complementados con patrones como el timeout vía race o el paralelismo limitado sobre all, cubren la mayor parte de las necesidades reales de orquestación asíncrona.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, JavaScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de JavaScript

Explora más contenido relacionado con JavaScript y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Diferenciar los cuatro combinators (all, allSettled, race, any) y cuándo usar cada uno.
  • Ejecutar promesas en paralelo preservando el orden de los resultados.
  • Manejar escenarios con fallos parciales usando Promise.allSettled.
  • Aplicar Promise.race para timeouts y cancelaciones.
  • Usar Promise.any para escenarios de redundancia o failover.
  • Entender AggregateError y el comportamiento ante iterables vacíos.