learning-front

Nivel 3 · JavaScript moderno y asíncrono

Promesas y async/await

De los callbacks a async/await: manejar lo que tarda sin bloquear ni anidar a lo loco, con su manejo de errores y la carga en paralelo con Promise.all.

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:

javascript
// 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”:

javascript
// 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.

javascript
// 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.

javascript
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:

  • async delante de una función: la marca como asíncrona. Una función async siempre devuelve una promesa.
  • await delante 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.)
javascript
// 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:

javascript
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.

javascript
// 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).

javascript
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. Define cargarHeroes() y cargarWinrate(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.
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.