Ya sabes lo esencial de las promesas y conoces Promise.all para lanzar varias a la vez. Pero
Promise.all tiene una pega importante —si una falla, se cae todo— y hay más formas de
combinar promesas que encajan mejor según el caso: esperar a todas sin rendirse al primer fallo,
quedarte con la más rápida, o cancelar una petición que ya no interesa. Aquí cerramos el tema con
las herramientas que usa el código real.
Seguimos con el Team Builder. Vamos a cargar el winrate de varios héroes a la vez, sabiendo que alguna carga puede fallar o tardar demasiado, y a reaccionar sin que se nos caiga la pantalla.
Recordatorio: Promise.all es “todo o nada”#
Promise.all ya lo viste a fondo en Promesas y async/await: recibe un array de promesas, las
espera todas y se resuelve con un array de resultados en el mismo orden de entrada. Aquí solo
necesitas recordar su pega, que es justo el problema que vamos a resolver: si una sola rechaza,
Promise.all rechaza entero y pierdes también las que sí habían llegado (un await Promise.all([...]) cae directo al catch, sin ningún dato).
Para “todo o nada” está bien (o cargan todos los datos críticos o no sigues). Pero muchas veces quieres lo contrario: lo que llegue, que llegue.
Promise.allSettled: espera a todas, nunca rechaza#
Promise.allSettled espera a que todas terminen —cumplan o fallen— y nunca rechaza. Te da un
array donde cada elemento describe su resultado:
Un detalle importante:
Promise.all(y tambiénPromise.allSettled) devuelve los resultados en el mismo orden del array de entrada, no en el orden en que se resolvieron. Si pasas[cargarTracer, cargarMercy, cargarGenji], el array de vuelta siempre será[resultadoTracer, resultadoMercy, resultadoGenji], aunque Genji llegue primero. Puedes confiar en ese orden para saber qué resultado le corresponde a qué héroe.
{ status: "fulfilled", value }si la promesa cumplió.{ status: "rejected", reason }si falló.
const resultados = await Promise.allSettled([
cargarWinrate("Tracer"),
// esta falla, pero no tumba al resto
cargarWinrate("Bastion"),
cargarWinrate("Mercy"),
]);
for (const r of resultados) {
// Cada uno trae su status: decidimos qué hacer con cada caso.
if (r.status === "fulfilled") console.log("OK: " + r.value);
else console.log("Falló: " + r.reason.message);
}Es la herramienta para carga resiliente: pides varias cosas y pintas las que lleguen, marcando las que no. Un fallo aislado no arruina la pantalla.
Promise.race: la primera en terminar (patrón timeout)#
Promise.race se resuelve (o rechaza) con la primera promesa que termine, gane o falle. Su uso
estrella es ponerle un límite de tiempo a algo: corres la operación contra un temporizador que
rechaza al pasar el tiempo, y gana lo que ocurra antes.
// Un temporizador que rechaza pasados `ms` milisegundos.
function timeout(ms) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error("demasiado lento")), ms);
});
}
// O llegan los datos antes de 1 s, o salta el timeout. Lo que pase primero.
const datos = await Promise.race([cargarWinrate("Genji"), timeout(1000)]);Si cargarWinrate tarda menos de un segundo, race se resuelve con sus datos. Si tarda más, gana el
timeout y race rechaza: lo tratas como cualquier error.
Promise.any: la primera que CUMPLA#
Promise.any se parece a race, pero con un matiz clave: ignora los fallos y se queda con la
primera que cumple. Solo rechaza si todas fallan.
// Varias réplicas del mismo servidor; nos vale la primera que responda bien.
const winrate = await Promise.any([
cargarDe("servidor-1"),
// si el 1 falla, seguimos esperando a este
cargarDe("servidor-2"),
cargarDe("servidor-3"),
]);
// Si TODAS fallan, any rechaza con un AggregateError (la suma de los fallos).La diferencia con race: race se queda con la primera que termine (aunque sea un fallo);
any espera hasta tener un éxito. Úsalo cuando tienes varias fuentes equivalentes y te basta la
primera que funcione.
Ejemplo ejecutable con el roster: dos cargas fallan antes de que llegue una válida, y any nos da
esa única que funcionó:
// Simulamos tres cargas: Bastion y Sigma fallan, Tracer responde bien.
function cargarWinrate(nombre) {
return new Promise(function(resolve, reject) {
// solo Tracer está en la base
var tabla = { Tracer: 0.65 };
setTimeout(function() {
if (tabla[nombre] !== undefined) {
// cumple con el valor
resolve(nombre + ': ' + Math.round(tabla[nombre] * 100) + '%');
} else {
// falla: no está en la tabla
reject(new Error('sin datos de ' + nombre));
}
}, 200);
});
}
Promise.any([
// falla: no está en la tabla
cargarWinrate('Bastion'),
// falla: tampoco está
cargarWinrate('Sigma'),
// cumple: devuelve "Tracer: 65%"
cargarWinrate('Tracer'),
])
.then(function(resultado) {
// any ignora los dos fallos anteriores y llega aquí con el primero que cumplió
// "any: primer exito -> Tracer: 65%"
console.log('any: primer exito -> ' + resultado);
})
.catch(function(error) {
// solo entraría aquí si Bastion, Sigma Y Tracer hubieran fallado todos
console.log('any: fallaron todas -> ' + error.message);
});
// ── Caso límite: TODAS fallan -> AggregateError ────────────────────────────
// Si ninguna cumple, any rechaza con un AggregateError: contiene todos los fallos.
Promise.any([
// falla: no está en la tabla
cargarWinrate('Bastion'),
// falla: tampoco está
cargarWinrate('Sigma'),
// falla: tampoco
cargarWinrate('Doomfist'),
])
.then(function(resultado) {
// no debería llegar aquí: todas fallan
console.log('ganó alguna: ' + resultado);
})
.catch(function(error) {
// AggregateError acumula todos los rechazos en error.errors
// "sin datos de Bastion, sin datos de Sigma, sin datos de Doomfist"
console.log('todas fallaron: ' + error.errors.map(function(e) { return e.message; }).join(', '));
});AbortController: cancelar lo que ya no interesa#
A veces lanzas una petición y, antes de que termine, deja de importarte: el usuario reescribe la
búsqueda, cambia de pantalla, cierra el componente. Dejarla correr gasta red y puede pintar datos
viejos. Para cancelar está AbortController:
// el "mando a distancia" para cancelar
const controlador = new AbortController();
// Le pasamos su signal al fetch: queda atado a este controlador.
fetch("https://api.tuequipo.dev/heroes", { signal: controlador.signal })
.then((res) => res.json())
.catch((error) => {
// Al cancelar, el fetch rechaza con un AbortError: lo distinguimos.
if (error.name === "AbortError") console.log("Cancelado a propósito");
else console.log("Otro fallo: " + error.message);
});
// En cuanto ya no interesa (nueva búsqueda, componente desmontado…):
// la petición se cancela y su promesa rechaza
controlador.abort();controlador.abort() cancela la petición en curso. Lo combinarás muchísimo cuando llegues a React:
cancelar el fetch de un efecto cuando el componente se desmonta o cuando cambian sus datos.
Cuándo cada uno#
| Combinador | Espera a… | Resultado |
|---|---|---|
Promise.all | todas | array de valores; rechaza con el primer fallo |
Promise.allSettled | todas | array con el status de cada una; nunca rechaza |
Promise.race | la primera en terminar | se resuelve o rechaza con esa, gane o falle |
Promise.any | la primera que cumpla | esa; rechaza solo si fallan todas (AggregateError) |
Y AbortController no combina promesas: cancela una operación en curso. Se usa junto a fetch.
import() dinámico: cargar un módulo bajo demanda#
En el capítulo de Módulos quedó algo pendiente: el import() con paréntesis. Ya puedes
entenderlo, porque es justo una promesa. El import normal (estático) se resuelve al cargar el
fichero, siempre, arriba del todo. import() es una función que devuelve una promesa del módulo:
carga ese código solo cuando lo llamas.
async function mostrarGraficas() {
// El módulo de gráficas (pesado) NO se carga al arrancar la página…
// …sino aquí, bajo demanda
const graficas = await import("./graficas.js");
// usamos lo que exporta, ya cargado
graficas.dibujar();
}¿Para qué? Para no cargar de golpe código que quizá no se use. Es la base del code-splitting
(partir el bundle en trozos que se cargan cuando hacen falta) y del lazy loading de React (que
verás en su nivel: React.lazy carga un componente solo al mostrarlo). Una página que arranca ligera
y trae lo pesado cuando toca.
Una nota relacionada: el top-level await. En un módulo puedes usar await en el nivel
superior, sin envolverlo en una función async:
// En un módulo, esto es válido directamente (sin función async alrededor):
const config = await import("./config.js");Es cómodo para inicializar un módulo con algo asíncrono, pero úsalo con cabeza: ese await retrasa
la carga del módulo que lo usa.
Pruébalo tú#
Edita el código y pulsa Ejecutar (o Ctrl+Enter): compara cómo all se cae entero por Bastion mientras allSettled
salva al resto, y cómo race resuelve con la carga antes de que salte el timeout. Luego baja el
timeout de 1000 a 100 y observa cómo ahora gana el temporizador.
Comprueba lo que sabes#
Pregunta 1 de 5
Cargas 5 recursos y quieres pintar los que lleguen aunque alguno falle. ¿Promise.all o Promise.allSettled?
Tu turno#
Carga el winrate de un roster donde uno falla y otro va muy lento. No pierdas lo que sí llega y, en
los niveles altos, ponle un timeout al lento y cancélalo. Cuando lo tengas (o si te atascas),
despliega las soluciones y fíjate en cómo el nivel Excelente combina allSettled, race y
AbortController para una carga resiliente, acotada y limpia.
Ejercicio · en esta página
Carga resiliente del roster
Carga el winrate de seis héroes a la vez. Bastion falla (no está en la base) y Reaper va muy lento. No dejes que un fallo se lleve por delante a los que sí llegan; en los niveles altos, ponle un timeout al lento y cancela su carga. Muestra los resultados por la consola.
Paso 1: Que funcione
- Cargas los winrates en paralelo y muestras el resultado en la consola.
- Vale usar Promise.all (aunque un fallo tumbe la carga entera).
Paso 2: Que esté pulido
- Usas Promise.allSettled para no perder lo que sí llega.
- Muestras por la consola los winrates que llegaron y marcas el que falló.
Paso 3: Que sea excelente
- Pones un timeout con Promise.race al héroe lento.
- Cancelas su carga con un AbortController.
- Manejas los errores en un solo sitio.
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "funciona"
//
// Carga todos los winrates en paralelo con Promise.all. Cuando TODO va bien, es
// perfecto: tardas lo de la más lenta, no la suma.
//
// Su límite: Promise.all es "todo o nada". En cuanto Bastion falla, all rechaza
// ENTERO y el catch se traga también a Tracer, Mercy, Genji y Ana, que sí habían
// llegado. Una sola pieza rota deja la consola sin resultados. Para datos que
// pueden fallar por separado, all es demasiado frágil.
// ════════════════════════════════════════════════════════════════════════════
var TABLA = {
Tracer: { winrate: 0.65, ms: 250 },
Mercy: { winrate: 0.68, ms: 400 },
Genji: { winrate: 0.48, ms: 300 },
Ana: { winrate: 0.63, ms: 500 },
Reaper: { winrate: 0.55, ms: 1500 },
};
var HEROES = ["Tracer", "Mercy", "Bastion", "Genji", "Ana", "Reaper"];
function cargarWinrate(nombre) {
return new Promise(function (resolve, reject) {
var dato = TABLA[nombre];
setTimeout(
function () {
if (dato) resolve({ nombre: nombre, winrate: dato.winrate });
else reject(new Error("sin datos de " + nombre));
},
dato ? dato.ms : 200,
);
});
}
async function cargarRoster() {
console.log("Cargando winrates…");
try {
// todas a la vez; si UNA rechaza (Bastion), all rechaza entero
var resultados = await Promise.all(
HEROES.map(function (n) {
return cargarWinrate(n);
}),
);
resultados.forEach(function (r) {
console.log(r.nombre + ": " + Math.round(r.winrate * 100) + "%");
});
} catch (error) {
// cae aquí por Bastion, y perdemos también a los que sí llegaron
console.log("No se pudo cargar el roster: " + error.message);
}
}
cargarRoster(); Por qué este nivel
- Promise.all carga todos los winrates en paralelo: cuando todo va bien, es perfecto (tardas lo de la más lenta, no la suma).
- Su límite: all es 'todo o nada'. En cuanto Bastion falla, rechaza entero y el catch se traga también a los que sí llegaron.
- Una pieza rota deja la pantalla vacía: demasiado frágil para datos que pueden fallar por separado.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL MEJOR — "pulido"
//
// Por qué mejora a OK:
// - Promise.allSettled en vez de Promise.all: espera a TODAS y NUNCA rechaza.
// Cada resultado es { status: 'fulfilled', value } o { status: 'rejected',
// reason }. Así mostramos los winrates que llegaron y marcamos solo el que
// falló (Bastion), sin perder al resto. Carga resiliente: "trae todo lo que
// puedas".
//
// Su límite respecto a Excelente: espera a Reaper, que tarda 1500 ms, sin ningún
// límite de tiempo. Si un héroe se quedara colgado para siempre, la carga no
// terminaría nunca. Falta ponerle un timeout y cancelar lo que se eternice.
// ════════════════════════════════════════════════════════════════════════════
var TABLA = {
Tracer: { winrate: 0.65, ms: 250 },
Mercy: { winrate: 0.68, ms: 400 },
Genji: { winrate: 0.48, ms: 300 },
Ana: { winrate: 0.63, ms: 500 },
Reaper: { winrate: 0.55, ms: 1500 },
};
var HEROES = ["Tracer", "Mercy", "Bastion", "Genji", "Ana", "Reaper"];
function cargarWinrate(nombre) {
return new Promise(function (resolve, reject) {
var dato = TABLA[nombre];
setTimeout(
function () {
if (dato) resolve({ nombre: nombre, winrate: dato.winrate });
else reject(new Error("sin datos de " + nombre));
},
dato ? dato.ms : 200,
);
});
}
async function cargarRoster() {
console.log("Cargando winrates…");
// allSettled espera a todas y nunca rechaza: cada una trae su status
var resultados = await Promise.allSettled(
HEROES.map(function (n) {
return cargarWinrate(n);
}),
);
resultados.forEach(function (r, i) {
var nombre = HEROES[i];
if (r.status === "fulfilled") {
console.log(nombre + ": " + Math.round(r.value.winrate * 100) + "%");
} else {
// status === 'rejected': lo marcamos, pero no tumba a los demás
console.log(nombre + ": sin datos (" + r.reason.message + ")");
}
});
}
cargarRoster(); Por qué es mejor que el anterior
- Promise.allSettled espera a todas y nunca rechaza: pintamos los winrates que llegaron y marcamos solo Bastion, sin perder al resto.
- Carga resiliente: 'trae todo lo que puedas'. Cada resultado trae su status (fulfilled o rejected).
- Su límite: espera a Reaper (1500 ms) sin ningún timeout; si un héroe se colgara, no terminaría nunca.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL EXCELENTE — "óptimo"
//
// Por qué mejora a Mejor:
// - Cada carga corre contra un LÍMITE DE TIEMPO con Promise.race: si tarda más
// de la cuenta, gana el temporizador y la marcamos como "tardó demasiado".
// Reaper (1500 ms) cae aquí con un timeout de 900 ms.
// - Al saltar el timeout, un AbortController CANCELA la carga en curso (su
// signal) en vez de dejarla corriendo a lo tonto: como un fetch que abortas
// cuando el usuario cambia de búsqueda o el componente se desmonta.
// - Sigue usando allSettled, así que un fallo (Bastion) o un timeout (Reaper)
// no se llevan por delante a los demás. El manejo de cada caso vive en UN
// sitio (la función evaluar), no repartido.
//
// Idea de fondo: resiliente (no se cae), acotado en el tiempo (no se cuelga) y
// limpio al cancelar (no malgasta trabajo). Los tres combinadores en su sitio.
// ════════════════════════════════════════════════════════════════════════════
var TABLA = {
Tracer: { winrate: 0.65, ms: 250 },
Mercy: { winrate: 0.68, ms: 400 },
Genji: { winrate: 0.48, ms: 300 },
Ana: { winrate: 0.63, ms: 500 },
Reaper: { winrate: 0.55, ms: 1500 },
};
var HEROES = ["Tracer", "Mercy", "Bastion", "Genji", "Ana", "Reaper"];
// API cancelable: si abortan su signal, paramos el trabajo (limpiamos el timer)
function cargarWinrate(nombre, opciones) {
opciones = opciones || {};
return new Promise(function (resolve, reject) {
var dato = TABLA[nombre];
var id = setTimeout(
function () {
if (dato) resolve({ nombre: nombre, winrate: dato.winrate });
else reject(new Error("sin datos de " + nombre));
},
dato ? dato.ms : 200,
);
if (opciones.signal) {
opciones.signal.addEventListener("abort", function () {
clearTimeout(id);
});
}
});
}
// Pide un winrate con límite de tiempo. Si se pasa, cancela la carga y lanza timeout.
function cargarConTimeout(nombre, ms) {
// el mando para cancelar
var ctrl = new AbortController();
var carga = cargarWinrate(nombre, { signal: ctrl.signal });
var limite = new Promise(function (_, reject) {
setTimeout(function () {
// se acabó el tiempo: cancela la carga en curso
ctrl.abort();
reject(new Error("timeout"));
}, ms);
});
// race: gana la que termine antes, la carga real o el límite de tiempo
return Promise.race([carga, limite]);
}
// Un único sitio que traduce cada resultado de allSettled a su línea de consola
function evaluar(resultado, nombre) {
if (resultado.status === "fulfilled") {
return nombre + ": " + Math.round(resultado.value.winrate * 100) + "%";
}
if (resultado.reason.message === "timeout") {
return nombre + ": tardó demasiado (cancelado)";
}
return nombre + ": sin datos";
}
async function cargarRoster() {
console.log("Cargando winrates…");
var resultados = await Promise.allSettled(
HEROES.map(function (n) {
return cargarConTimeout(n, 900);
}),
);
resultados.forEach(function (r, i) {
console.log(evaluar(r, HEROES[i]));
});
}
cargarRoster(); Por qué es mejor que el anterior
- Cada carga corre contra un timeout con Promise.race; si se pasa, un AbortController la cancela (como un fetch que abortas). Reaper sale cancelado.
- Sigue con allSettled, así que ni el fallo de Bastion ni el timeout de Reaper tumban a los demás.
- Resiliente (no se cae), acotada en el tiempo (no se cuelga) y limpia al cancelar; el manejo de cada caso vive en un sitio.