Hasta ahora tu código era síncrono: cada línea termina antes de que empiece la siguiente, en orden. Pero algunas cosas tardan —un temporizador, leer un fichero y, sobre todo, pedir datos a un servidor— y no puedes dejar la página congelada esperando. La asincronía es cómo JavaScript dice “esto tarda; sigue con lo tuyo y te aviso cuando esté listo”.
Aún no usamos red (eso es el próximo capítulo). Aquí simulamos “algo que tarda” con
setTimeout, que ejecuta una función pasados unos milisegundos.
El problema: el resultado todavía no está#
Imagina pedir los héroes a un servidor. La respuesta no es inmediata. Si escribieras esto:
// tarda 600ms
const heroes = pedirHeroesAlServidor();
// ¿qué hay aquí? Todavía nada
console.log(heroes);…el console.log correría antes de que los datos lleguen. Necesitamos una forma de decir
“haz esto cuando lleguen”. La primera solución fue pasar una función (un callback) que
se llama al terminar. En un solo nivel funciona, pero en cuanto necesitas encadenar varios pasos
el código se convierte en una pirámide ilegible, el famoso “callback hell”:
// Cargamos el roster…
pedirHeroesAlServidor(function(heroes) {
// …y cuando llega, cargamos el winrate del primero…
pedirWinrate(heroes[0].nombre, function(winrate) {
// …y cuando llega ese, cargamos el historial…
pedirHistorial(heroes[0].nombre, function(historial) {
// cada nivel añade una sangría; el código crece hacia la derecha
console.log('Héroe: ' + heroes[0].nombre);
console.log('Winrate: ' + winrate);
console.log('Partidas: ' + historial.total);
});
});
});Cada paso dependiente del anterior añade un nivel de anidación. Con tres o cuatro pasos el código
es difícil de leer y de depurar. La solución moderna son las promesas y, encima de ellas,
async/await.
La promesa: un valor que llegará#
Una promesa es un objeto que representa un resultado futuro. Nace pendiente (pending) y acaba de una de dos formas: cumplida (fulfilled, con un valor) o rechazada (rejected, con un error). Una función que tarda devuelve una promesa en lugar del valor.
// new Promise recibe una función con dos "interruptores": resolve y reject.
// Llamar a resolve(valor) cumple la promesa; reject(error) la rechaza.
function cargarHeroes() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// a los 600ms, cumple con este valor
resolve(['Tracer', 'Mercy', 'Reinhardt']);
}, 600);
});
}
// cargarHeroes() no devuelve los héroes: devuelve una PROMESA de los héroes.En el día a día rara vez creas promesas con new Promise a mano: te las dan ya hechas
(fetch, librerías…). Lo que harás siempre es consumirlas.
Consumir con .then / .catch / .finally#
El primer modo de consumir una promesa: .then(fn) registra qué hacer cuando se cumpla
(recibe el valor); .catch(fn) qué hacer si se rechaza (recibe el error); .finally(fn) se
ejecuta pase lo que pase.
cargarHeroes()
.then((heroes) => {
// Se ejecuta cuando la promesa se cumple; heroes es el valor.
console.log('Llegaron: ' + heroes.join(', '));
})
.catch((error) => {
// Se ejecuta si la promesa se rechaza; error es el motivo.
console.log('Falló: ' + error.message);
})
.finally(() => {
// Siempre: ideal para apagar un "cargando".
console.log('Petición terminada.');
});Lo importante: el código después de esta llamada sigue corriendo sin esperar. La página no se
bloquea; el .then se ejecutará más tarde, cuando los datos lleguen.
async/await: escribir asíncrono como si fuera síncrono#
Encadenar .then se vuelve farragoso. async/await es azúcar sintáctico sobre las promesas que
deja escribir código asíncrono de arriba abajo, como el de siempre.
Dos piezas:
asyncdelante de una función: la marca como asíncrona. Una funciónasyncsiempre devuelve una promesa.awaitdelante de una promesa: pausa esa función hasta que la promesa se resuelve, y devuelve su valor. (Pausa solo esa función, no la página.)
// async habilita el uso de await dentro.
async function mostrarHeroes() {
// await espera a que cargarHeroes() se resuelva y nos da el valor directamente.
const heroes = await cargarHeroes();
// corre después de llegar
console.log('Tengo ' + heroes.length + ' héroes');
}
mostrarHeroes();Compara: con .then, el valor vive dentro de una función anidada. Con await, vive en una
variable normal (heroes) y el código se lee en orden. Es la forma preferida hoy.
Errores con try/catch#
Si una promesa se rechaza, el await lanza ese error. Lo capturas con try/catch, igual que
cualquier otro error:
async function mostrarHeroes() {
try {
// si esto se rechaza…
const heroes = await cargarHeroes();
console.log('Llegaron: ' + heroes.join(', '));
} catch (error) {
// …el control salta aquí. error es el motivo del rechazo.
console.log('Algo falló: ' + error.message);
} finally {
// siempre
console.log('Listo (con éxito o no).');
}
}Es el equivalente exacto del .then().catch().finally(), pero con la sintaxis de manejo de
errores que ya conoces.
Un aviso importante: si llamas a una función async y no le pones try/catch ni .catch, y la
promesa se rechaza, ese error queda sin capturar (unhandled rejection). En el navegador
aparece un aviso en consola; en Node puede tumbar el proceso. La regla es simple: siempre que
hagas await o consumas una promesa, captura el posible error.
// MAL: si cargarHeroes rechaza, el error se pierde sin avisar
// llamada sin captura
mostrarHeroes();
// BIEN: envuelve el await en try/catch dentro de la función async
async function mostrarHeroes() {
try {
// si rechaza, salta al catch
const heroes = await cargarHeroes();
console.log('Llegaron: ' + heroes.join(', '));
} catch (error) {
// nunca queda sin capturar
console.log('Error capturado: ' + error.message);
}
}
// BIEN también: encadena .catch al llamarla desde fuera
mostrarHeroes().catch((error) => {
// captura lo que se escape
console.log('Error externo: ' + error.message);
});Varias cosas a la vez: Promise.all#
A veces necesitas varios datos independientes. Si los pides con await uno tras otro, cada
uno espera al anterior y sumas los tiempos. Si son independientes, lánzalos a la vez con
Promise.all, que recibe un array de promesas y se resuelve con un array de resultados cuando
todas terminan (y se rechaza si alguna falla).
async function cargarWinrates() {
// En serie (LENTO): cada await espera al anterior → 400 + 400 + 400 ms.
// const a = await cargarWinrate('Tracer');
// const b = await cargarWinrate('Mercy');
// const c = await cargarWinrate('Reinhardt');
// En paralelo (RÁPIDO): arrancan a la vez → ~400 ms en total.
const [a, b, c] = await Promise.all([
cargarWinrate('Tracer'),
cargarWinrate('Mercy'),
cargarWinrate('Reinhardt'),
]);
console.log('Winrates: ' + a + ', ' + b + ', ' + c);
}La regla: await en serie solo cuando un paso necesita el resultado del anterior. Si no
dependen entre sí, Promise.all. Un await dentro de un for convierte tareas independientes en
una cola en serie: tardas la suma de todos los tiempos. Promise.all las lanza a la vez y tardas
lo de la más lenta.
Pruébalo tú#
Edita el código y pulsa Ejecutar (o Ctrl+Enter): fíjate en el orden de los mensajes (1, 2, 3…), que demuestra
que el programa no se bloquea. Luego cambia 'Reinhardt' por 'Bastion' y observa cómo salta el
error.
Comprueba lo que sabes#
Pregunta 1 de 5
¿Qué es una promesa en JavaScript?
Tu turno#
Carga los héroes de una API falsa que tarda: muéstralos por la consola al llegar y controla
los errores. En el nivel alto, trae además el winrate de cada uno en paralelo. Cuando lo tengas
(o si te atascas), despliega las soluciones y compara .then, async/await y Promise.all.
La API falsa está incluida directamente en
solucion.js. DefinecargarHeroes()ycargarWinrate(nombre)— las mismas funciones que en el capítulo — y trabaja desde ahí.
Ejercicio · en esta página
Carga asíncrona de héroes
Los héroes llegan de una API falsa con retraso. Pide los datos, muéstralos por la consola al llegar y controla los errores. En el nivel alto, carga además el winrate de cada héroe en paralelo con Promise.all.
Paso 1: Que funcione
- Consumes la promesa (con .then o await) y muestras los héroes por la consola al llegar.
- Atrapas un posible error.
Paso 2: Que esté pulido
- Usas async/await.
- Controlas el error con try/catch.
- El estado de carga tiene un sitio claro (por ejemplo, finally).
Paso 3: Que sea excelente
- Cargas el winrate de cada héroe en paralelo con Promise.all.
- Combinas los datos sin mutar.
- Usas await en serie solo cuando un paso necesita el resultado del anterior.
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "que funcione"
//
// Consume la promesa con .then: cuando cargarHeroes() se resuelve, muestra
// los datos en consola. Con .catch atrapa un fallo mínimo. Funciona.
//
// Su límite (lo pule Mejor): si encadenas más pasos asíncronos, los .then se
// anidan y cuesta leerlos.
// ════════════════════════════════════════════════════════════════════════════
// ── API falsa ──────────────────────────────────────────────────────────────
function esperar(ms) {
return new Promise(function (r) {
setTimeout(r, ms);
});
}
async function cargarHeroes() {
await esperar(600);
return [
{ nombre: "Tracer", rol: "Daño" },
{ nombre: "Mercy", rol: "Apoyo" },
{ nombre: "Reinhardt", rol: "Tanque" },
];
}
// ── Solución ────────────────────────────────────────────────────────────────
// .then recibe el valor con el que se resolvió la promesa (los héroes).
// .catch se ejecuta si la promesa se rechaza.
cargarHeroes()
.then(function (heroes) {
console.log("Héroes cargados: " + heroes.length);
heroes.forEach(function (h) {
console.log(h.nombre + " (" + h.rol + ")");
});
})
.catch(function (error) {
console.log("Algo salió mal: " + error.message);
}); Por qué este nivel
- Consume la promesa con .then (recibe el valor) y .catch (atrapa el fallo). Muestra los datos en consola al llegar.
- Funciona, que es el primer requisito.
- Su límite: encadenar más pasos asíncronos anida .then y cuesta leerlo.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL MEJOR — "que esté pulido"
//
// Por qué mejora a OK:
// - async/await: el código asíncrono se lee de arriba abajo como si fuera
// síncrono. await "pausa" hasta que la promesa se resuelve y te da su valor.
// - try/catch/finally: el try envuelve lo que puede fallar, el catch muestra el
// error, y el finally se ejecuta SIEMPRE (útil para señalizar que terminó).
//
// Qué deja para Excelente: cargar datos extra (el winrate de cada héroe) EN
// PARALELO con Promise.all, en vez de uno tras otro.
// ════════════════════════════════════════════════════════════════════════════
// ── API falsa ──────────────────────────────────────────────────────────────
function esperar(ms) {
return new Promise(function (r) {
setTimeout(r, ms);
});
}
async function cargarHeroes() {
await esperar(600);
return [
{ nombre: "Tracer", rol: "Daño" },
{ nombre: "Mercy", rol: "Apoyo" },
{ nombre: "Reinhardt", rol: "Tanque" },
];
}
// ── Solución ────────────────────────────────────────────────────────────────
// Una función async para poder usar await dentro.
async function iniciar() {
try {
// espera y recibe el valor
var heroes = await cargarHeroes();
console.log("Héroes cargados: " + heroes.length);
heroes.forEach(function (h) {
console.log(h.nombre + " (" + h.rol + ")");
});
} catch (error) {
console.log("Algo salió mal: " + error.message);
} finally {
// siempre se ejecuta, haya error o no
console.log("Carga terminada.");
}
}
iniciar(); Por qué es mejor que el anterior
- async/await: el código asíncrono se lee de arriba abajo como si fuera síncrono.
- try/catch/finally: error controlado y estado de carga con su sitio claro (finally siempre se ejecuta).
- Su límite respecto a Excelente: aún no carga datos extra en paralelo.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL EXCELENTE — "óptimo"
//
// Por qué mejora a Mejor:
// - Carga el winrate de cada héroe EN PARALELO con Promise.all, no en fila.
// Si pidieras los winrates uno tras otro con await en un bucle (400ms cada
// uno), tardarías la SUMA. Con Promise.all arrancan todos a la vez y esperas
// al más lento: tardas ~400ms en total, no 400ms × N. Eso es una "cascada"
// evitada.
// - Funciones pequeñas y con un propósito: cargar todo (datos), mostrar
// (presentación). El error se controla en un solo sitio.
// - Inmutabilidad: combina héroe + winrate en objetos nuevos, sin mutar.
//
// Idea de fondo: cuando varias esperas NO dependen una de otra, lánzalas juntas.
// await en serie solo cuando el segundo paso necesita el resultado del primero.
// ════════════════════════════════════════════════════════════════════════════
// ── API falsa ──────────────────────────────────────────────────────────────
function esperar(ms) {
return new Promise(function (r) {
setTimeout(r, ms);
});
}
async function cargarHeroes() {
await esperar(600);
return [
{ nombre: "Tracer", rol: "Daño" },
{ nombre: "Mercy", rol: "Apoyo" },
{ nombre: "Reinhardt", rol: "Tanque" },
];
}
async function cargarWinrate(nombre) {
await esperar(400);
var tabla = { Tracer: 0.65, Mercy: 0.65, Reinhardt: 0.567 };
if (!(nombre in tabla)) throw new Error("Héroe desconocido: " + nombre);
return tabla[nombre];
}
// ── Solución ────────────────────────────────────────────────────────────────
// Trae héroes y, a la vez, el winrate de cada uno. Devuelve héroes enriquecidos.
async function cargarTodo() {
// este sí va primero: necesitamos los nombres para pedir los winrates
var heroes = await cargarHeroes();
// lanzamos TODAS las peticiones de winrate a la vez; Promise.all espera a todas
var winrates = await Promise.all(
heroes.map(function (h) {
return cargarWinrate(h.nombre);
}),
);
// winrates[i] corresponde a heroes[i]: los combinamos sin mutar
return heroes.map(function (h, i) {
return { nombre: h.nombre, rol: h.rol, winrate: winrates[i] };
});
}
function mostrar(heroes) {
console.log("Héroes cargados: " + heroes.length);
heroes.forEach(function (h) {
console.log(
h.nombre + " (" + h.rol + ") — " + (h.winrate * 100).toFixed(1) + "%",
);
});
}
async function iniciar() {
try {
var heroes = await cargarTodo();
mostrar(heroes);
} catch (error) {
console.log("Algo salió mal: " + error.message);
}
}
iniciar(); Por qué es mejor que el anterior
- Promise.all carga el winrate de cada héroe EN PARALELO: tardas lo de una petición, no la suma.
- Funciones pequeñas con un propósito; el error se controla en un solo sitio.
- Combina héroe + winrate en objetos nuevos, sin mutar. await en serie solo donde un paso necesita al anterior.