Terminaste la lección anterior con una pirámide de compra que funcionaba, pero cuyo coste era evidente: un objeto contexto para arrastrar el estado a mano, dos puntos de salida de error, una compensación anidada dentro de otro callback y la imposibilidad de lanzar en paralelo lo que era independiente. El diagnóstico fue claro: el lenguaje no ayudaba. try/catch no funcionaba, return no servía, finally no existía.

Las promesas devuelven todo eso. Una promesa es un objeto que representa un valor que todavía no está disponible, y que el lenguaje entiende: puede encadenarse, componerse, esperarse con await y capturarse con try/catch. Sobre ellas, async/await añade una capa de sintaxis que hace que el código asíncrono se lea exactamente igual que el síncrono, sin serlo.

En esta lección vas a entender qué es realmente una promesa y sus tres estados, a convertir con util.promisify las funciones de callback que escribiste ayer, a reescribir el flujo de compra de Escena Viva hasta dejarlo plano y legible, y a dominar la distinción que separa al principiante del profesional: cuándo esperar en serie y cuándo lanzar en paralelo. Terminarás con los patrones que usarás cada día —reintentos, tiempos límite y una función dormir— y sabiendo por qué un rechazo no manejado tumba tu proceso en Node moderno.

Contenido

  1. Qué es una promesa y sus tres estados
  2. Consumir promesas: .then, .catch y .finally
  3. El encadenamiento plano frente a la pirámide
  4. Crear promesas: new Promise y util.promisify
  5. async y await: las tres reglas
  6. El flujo de compra reescrito, lado a lado
  7. Secuencial frente a concurrente: el error del for con await
  8. Promise.all, allSettled, race y any
  9. Propagación de errores y rechazos no manejados
  10. await de nivel superior
  11. Patrones útiles: dormir, reintentos y tiempo límite
  12. Tabla resumen: callbacks, promesas y async/await

  1. Qué es una promesa y sus tres estados

Una promesa (Promise) es un objeto que representa el resultado futuro de una operación asíncrona. No es el valor: es un recibo que puedes guardar, pasar a otras funciones y consultar, y que en algún momento se convertirá en un valor o en un error.

Una promesa está siempre en uno de tres estados:

stateDiagram-v2
    [*] --> Pendiente: se crea la promesa
    Pendiente --> Cumplida: resolve(valor)
    Pendiente --> Rechazada: reject(error)
    Cumplida --> [*]: .then(valor)
    Rechazada --> [*]: .catch(error)

    note right of Pendiente
        La operación está en curso.
        No hay valor todavía.
    end note
    note right of Cumplida
        fulfilled: hay un valor.
        Estado FINAL.
    end note
    note right of Rechazada
        rejected: hay un error.
        Estado FINAL.
    end note
Estado Nombre en inglés Significado
Pendiente pending La operación aún no ha terminado
Cumplida fulfilled Terminó bien y hay un valor
Rechazada rejected Terminó mal y hay un motivo (normalmente un Error)

Y tres propiedades que definen su comportamiento y que hay que grabar a fuego:

  1. Una promesa cambia de estado exactamente una vez. De pendiente pasa a cumplida o a rechazada, y ahí se queda para siempre. Se dice que está asentada (settled). Esto resuelve por decreto el problema de "el callback se llamó dos veces": es imposible.
  2. El resultado es inmutable. Una vez cumplida con un valor, ese valor no cambia.
  3. Puedes suscribirte cuando quieras, incluso tarde. Si te suscribes a una promesa ya cumplida, tu función se ejecuta igualmente (en la cola de microtareas). Con un callback, si llegas tarde, te lo has perdido.

Puedes verlo en el REPL, que ya conoces de la lección El REPL de Node.js:

> const p = new Promise((resolve) => setTimeout(() => resolve('listo'), 2000));
> p
Promise { <pending> }          // Antes de los 2 segundos

// ... esperas dos segundos ...

> p
Promise { 'listo' }            // Ya asentada, y con su valor visible

> Promise.reject(new Error('fallo'))
Promise { <rejected> Error: fallo ... }

  1. Consumir promesas: .then, .catch y .finally

Una promesa se consume con tres métodos:

// src/laboratorio/consumir-promesa.js
const fs = require('node:fs/promises');   // La version con promesas de fs

fs.readFile('datos/eventos.json', 'utf8')
  .then((contenido) => {
    // Se ejecuta si la promesa se CUMPLE. Recibe el valor.
    const catalogo = JSON.parse(contenido);
    console.log(`Cargados ${catalogo.length} eventos`);
  })
  .catch((error) => {
    // Se ejecuta si la promesa se RECHAZA en cualquier punto anterior.
    console.error(`No se pudo cargar el catalogo: ${error.message}`);
  })
  .finally(() => {
    // Se ejecuta SIEMPRE, haya ido bien o mal. Sin argumentos.
    console.error('[catalogo] intento de carga finalizado');
  });

Fíjate en node:fs/promises: Node ofrece versiones con promesas de sus módulos principales. fs/promises, dns/promises, timers/promises y stream/promises existen precisamente para no tener que convertir nada a mano.

Método Cuándo se ejecuta Qué recibe Qué devuelve
.then(fn) Al cumplirse El valor Una nueva promesa
.catch(fn) Al rechazarse El motivo del rechazo Una nueva promesa
.finally(fn) Siempre, al asentarse Nada Una promesa con el mismo resultado

Esa columna de la derecha es la clave de todo lo que viene: cada método devuelve una promesa nueva, y eso es lo que permite encadenar.

Dos matices sobre .finally que se olvidan a menudo:

  • No recibe argumentos, porque no sabe (ni le importa) si hubo éxito o error. Es para limpieza: cerrar un fichero, quitar un indicador de carga, liberar un recurso.
  • No altera el resultado. Si la promesa se rechazó, sigue rechazada después del finally.

  1. El encadenamiento plano frente a la pirámide

Aquí está la primera gran ganancia. Recuerda la forma de la pirámide:

// CALLBACKS: cada paso dentro del anterior.
buscarEvento(eventoId, (error, evento) => {
  if (error) return console.error(error.message);
  comprobarAforo(sesionId, cantidad, (error, sesion) => {
    if (error) return console.error(error.message);
    crearPedido(usuarioId, sesion, cantidad, (error, pedido) => {
      if (error) return console.error(error.message);
      console.log(pedido.id);
    });
  });
});

Y ahora con promesas:

// PROMESAS: cada paso al MISMO nivel, con un unico catch al final.
buscarEvento(eventoId)
  .then((evento) => comprobarAforo(sesionId, cantidad))
  .then((sesion) => crearPedido(usuarioId, sesion, cantidad))
  .then((pedido) => console.log(pedido.id))
  .catch((error) => console.error(error.message));

La regla que hace esto posible es sencilla y potente:

Si dentro de un .then devuelves una promesa, la cadena espera a que se asiente antes de continuar.

Y la segunda regla, igual de importante:

Un rechazo en cualquier punto de la cadena salta todos los .then siguientes hasta encontrar un .catch.

flowchart LR
    A["buscarEvento"] -->|"cumplida"| B[".then<br/>comprobarAforo"]
    B -->|"cumplida"| C[".then<br/>crearPedido"]
    C -->|"cumplida"| D[".then<br/>mostrar"]
    D --> E[".catch"]
    A -.->|"rechazada"| E
    B -.->|"rechazada"| E
    C -.->|"rechazada"| E
    D -.->|"rechazada"| E

Un único punto de tratamiento de errores para toda la cadena. Compáralo con los cinco if (error) return de la lección anterior.

El error clásico: olvidar el return

// MAL: la cadena NO espera a crearPedido.
buscarEvento(eventoId)
  .then((evento) => {
    crearPedido(usuarioId, evento, 2);   // Falta el return
  })
  .then((pedido) => {
    console.log(pedido.id);   // TypeError: pedido es undefined
  });

// BIEN
buscarEvento(eventoId)
  .then((evento) => {
    return crearPedido(usuarioId, evento, 2);
  })
  .then((pedido) => {
    console.log(pedido.id);
  });

// BIEN, en forma corta: la flecha sin llaves devuelve implicitamente.
buscarEvento(eventoId)
  .then((evento) => crearPedido(usuarioId, evento, 2))
  .then((pedido) => console.log(pedido.id));

Este es a las promesas lo que el return tras el error era a los callbacks: el fallo número uno. La forma corta con flecha sin llaves lo evita de raíz, y por eso se prefiere.

  1. Crear promesas: new Promise y util.promisify

4.1 new Promise

El constructor recibe una función —llamada ejecutor— con dos parámetros: resolve y reject.

// src/utiles/dormir.js
// Devuelve una promesa que se cumple pasados los milisegundos indicados.

function dormir(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

module.exports = { dormir };
console.log('Antes');
dormir(1000).then(() => console.log('Un segundo despues'));

El ejecutor se ejecuta inmediatamente y de forma síncrona al construir la promesa. Lo asíncrono es el setTimeout de dentro, no el constructor.

Una versión con rechazo, aplicada a Escena Viva:

// Simula el cobro con la pasarela de pago.
function cobrarPedido(pedido) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (pedido.totalCentimos > 20000) {
        const error = new Error(
          `Pago rechazado: ${(pedido.totalCentimos / 100).toFixed(2)} EUR supera el limite`
        );
        error.codigo = 'PAGO_RECHAZADO';
        reject(error);
        return;
      }
      resolve({ ...pedido, estado: 'pagado' });
    }, 40);
  });
}

Reglas del constructor:

  • resolve y reject no interrumpen la función. Igual que con los callbacks, pon un return después.
  • reject siempre con un objeto Error. reject('algo falló') es legal y es mala práctica: pierdes la traza.
  • Una excepción lanzada dentro del ejecutor se convierte en un rechazo automáticamente. Esta sí la captura la promesa.

Antipatrón importante: el "constructor antipattern". Si ya tienes una promesa, no la envuelvas en otra.

// MAL
function cargar() {
  return new Promise((resolve, reject) => {
    fs.readFile('a.json', 'utf8').then(resolve).catch(reject);
  });
}
// BIEN
function cargar() {
  return fs.readFile('a.json', 'utf8');
}

new Promise es solo para envolver algo que todavía no es una promesa: un callback, un evento, un temporizador.

4.2 util.promisify: convertir los callbacks de ayer

Aquí está la herramienta que convierte todo el trabajo de la lección anterior. util.promisify toma una función que sigue la convención error-first y devuelve otra que devuelve una promesa.

// src/laboratorio/promisificar.js
const { promisify } = require('node:util');

// Las funciones de la leccion anterior, con callback error-first.
// function buscarEvento(id, callback) { ... }
// function buscarSesion(sesionId, callback) { ... }
// function reservarEntradas(sesionId, cantidad, callback) { ... }

const buscarEventoP = promisify(buscarEvento);
const buscarSesionP = promisify(buscarSesion);
const reservarEntradasP = promisify(reservarEntradas);

// Y ya se usan como promesas.
buscarEventoP('evt-002')
  .then((evento) => console.log(`Encontrado: ${evento.titulo}`))
  .catch((error) => console.error(`[${error.codigo}] ${error.message}`));

Cómo funciona por dentro, para que no sea magia:

// Version simplificada de lo que hace promisify.
function promisificar(funcionConCallback) {
  return function (...argumentos) {
    return new Promise((resolve, reject) => {
      funcionConCallback(...argumentos, (error, resultado) => {
        if (error) {
          reject(error);
          return;
        }
        resolve(resultado);
      });
    });
  };
}

Requisitos para que promisify funcione:

Requisito Si no se cumple
El callback es el último parámetro No funciona
El callback tiene la firma (error, resultado) El valor resuelto será incorrecto
El callback se llama una sola vez Las llamadas extra se ignoran (la promesa ya está asentada)

Fíjate en esa última fila: la promesa te protege del bug de "llamar dos veces al callback". Es una de esas ventajas silenciosas que se agradecen mucho.

Si la función devuelve varios valores (no es error-first estándar), existe util.promisify.custom para definir la conversión a mano. Es poco frecuente; cuando lo necesites, la documentación de node:util lo cubre.

  1. async y await: las tres reglas

async/await no es un mecanismo nuevo: es azúcar sintáctico sobre promesas. Por debajo hay exactamente las mismas promesas y la misma cola de microtareas. Lo que cambia es cómo se escribe.

Regla 1: una función async siempre devuelve una promesa

async function obtenerTitulo() {
  return 'Concierto de Otono';   // Devolvemos una cadena...
}

console.log(obtenerTitulo());              // Promise { 'Concierto de Otono' }
obtenerTitulo().then((t) => console.log(t)); // Concierto de Otono

Aunque devuelvas un número, undefined o nada, el resultado es una promesa. Y si lanzas una excepción dentro, la promesa se rechaza en lugar de propagar la excepción:

async function fallar() {
  throw new Error('algo salio mal');
}

fallar().catch((error) => console.error(error.message));   // algo salio mal

Regla 2: await solo pausa la función que lo contiene

Esta es la regla que más se malinterpreta. await no bloquea el proceso ni el hilo. Pausa únicamente la función async donde aparece; el resto del programa sigue funcionando con normalidad.

// src/laboratorio/await-no-bloquea.js
const { dormir } = require('../utiles/dormir.js');

async function tareaLenta() {
  console.log('  [lenta] empiezo');
  await dormir(300);
  console.log('  [lenta] termino');
}

// Un latido que demuestra que el proceso sigue vivo.
const latido = setInterval(() => console.log('latido'), 100);

tareaLenta().then(() => clearInterval(latido));

console.log('Esta linea se ejecuta ANTES de que tareaLenta termine');
  [lenta] empiezo
Esta linea se ejecuta ANTES de que tareaLenta termine
latido
latido
  [lenta] termino

Durante los 300 ms de espera el bucle de eventos siguió girando y disparando el temporizador. Compáralo con el bloqueo síncrono de la lección de arquitectura, donde el latido desaparecía por completo. await cede el control; un bucle while lo retiene.

Y await solo es válido dentro de una función async (o en el nivel superior de un módulo ES, apartado 10):

// SyntaxError: await is only valid in async functions
function mal() {
  const evento = await buscarEventoP('evt-001');
}

Regla 3: try/catch vuelve a funcionar

Esta es la razón por la que existe async/await.

// src/laboratorio/try-catch-funciona.js

async function mostrarEvento(id) {
  try {
    const evento = await buscarEventoP(id);
    console.log(`Encontrado: ${evento.titulo}`);
  } catch (error) {
    // SI se captura. El await convierte el rechazo en una excepcion normal.
    console.error(`[${error.codigo}] ${error.message}`);
  } finally {
    // Y finally tambien funciona: aqui va la limpieza.
    console.error(`[consulta] terminada para ${id}`);
  }
}

mostrarEvento('evt-999');
[EVENTO_NO_ENCONTRADO] Evento no encontrado: evt-999
[consulta] terminada para evt-999

Recuperar try/catch/finally no es solo comodidad sintáctica: significa que el modelo de errores del lenguaje vuelve a aplicarse al código asíncrono. Los errores se propagan hacia arriba por la pila de llamadas async, se capturan donde tenga sentido capturarlos, y finally garantiza la limpieza. Toda la contabilidad manual de la lección anterior desaparece.

  1. El flujo de compra reescrito, lado a lado

Momento de cobrar el premio. Este es el mismo flujo del ejercicio 3 de la lección anterior, ahora con async/await.

Antes, con callbacks (resumido a su estructura):

function comprarEntradas(usuarioId, eventoId, sesionId, cantidad, alTerminar) {
  const contexto = { usuarioId, eventoId, sesionId, cantidad, aforoReservado: false };

  buscarEvento(eventoId, alEncontrarEvento);

  function alEncontrarEvento(error, evento) {
    if (error) return fallo('buscar-evento', error);
    contexto.evento = evento;
    reservarEntradas(sesionId, cantidad, alReservar);
  }
  function alReservar(error, reserva) { /* ... */ }
  function alBuscarSesion(error, resultado) { /* ... */ }
  function alCrearPedido(error, pedido) { /* ... */ }
  function alCobrar(error, pedidoPagado) { /* ... */ }
  function alEmitir(error, entradas) { /* ... */ }
  function fallo(paso, error) { /* ... */ }
  function compensarYFallar(paso, error) {
    liberarAforo(sesionId, cantidad, (errorLiberar) => { /* ... */ });
  }
}

Después, con async/await:

// src/laboratorio/compra-async.js
// El mismo flujo de compra, con promesas. 30 lineas en lugar de 70.

async function comprarEntradas(usuarioId, eventoId, sesionId, cantidad) {
  const evento = await buscarEventoP(eventoId);

  // A partir de aqui hay aforo reservado que puede haber que devolver.
  const reserva = await reservarEntradasP(sesionId, cantidad);

  try {
    const { sesion } = await buscarSesionP(sesionId);
    const pedido = await crearPedidoP(usuarioId, sesion, cantidad);
    const pedidoPagado = await cobrarPedido(pedido);
    const entradas = await emitirEntradasP(pedidoPagado);

    return { evento, sesion, pedido: pedidoPagado, entradas };
  } catch (error) {
    // Compensacion: liberamos el aforo que habiamos reservado.
    const libres = await liberarAforoP(sesionId, cantidad);
    console.error(`[compensacion] aforo liberado en ${sesionId}, quedan ${libres} libres`);

    // Y relanzamos: quien nos llamo decide que hacer.
    throw error;
  }
}

Y su uso:

async function principal() {
  try {
    const compra = await comprarEntradas('asis-001', 'evt-002', 'ses-002-2', 3);

    console.log(`Pedido ${compra.pedido.id} - ${compra.pedido.estado}`);
    console.log(`  Evento : ${compra.evento.titulo}`);
    console.log(`  Importe: ${(compra.pedido.totalCentimos / 100).toFixed(2)} EUR`);
    for (const entrada of compra.entradas) {
      console.log(`  ${entrada.codigo}  ${entrada.estado}`);
    }
  } catch (error) {
    console.error(`[${error.codigo || 'ERROR'}] ${error.message}`);
    process.exitCode = 1;
  }
}

principal();

Compara punto por punto:

Aspecto Callbacks async/await
Líneas del flujo ~70 ~30
Niveles de indentación 2 (con la técnica de aplanado) 1
Objeto contexto para el estado Necesario Innecesario: son variables locales normales
Puntos de tratamiento de errores 2 (fallo y compensarYFallar) 1 (catch)
Compensación Callback anidado con su propio error 2 líneas dentro del catch
Devolver el resultado Imposible; hay que pasar un callback return normal
Orden de lectura Saltando entre funciones De arriba abajo

Y fíjate en el detalle más elegante: evento, reserva, sesion, pedido y entradas son variables locales corrientes, visibles en todo el cuerpo de la función. Aquello de arrastrar el contexto por cinco callbacks ha desaparecido, y no porque hayamos sido más listos: porque el lenguaje ahora entiende la espera.

  1. Secuencial frente a concurrente: el error del for con await

async/await es tan cómodo que induce a un error de rendimiento muy frecuente y muy caro. Míralo:

// src/laboratorio/secuencial-vs-paralelo.js
// MAL: los tres eventos son independientes, pero se cargan en serie.

async function cargarEventosEnSerie(ids) {
  const eventos = [];
  for (const id of ids) {
    const evento = await buscarEventoP(id);   // Espera a que termine el anterior
    eventos.push(evento);
  }
  return eventos;
}

const inicio = Date.now();
cargarEventosEnSerie(['evt-001', 'evt-002', 'evt-003']).then((eventos) => {
  console.log(`En serie: ${eventos.length} eventos en ${Date.now() - inicio} ms`);
});
En serie: 3 eventos en 124 ms

Con 40 ms de latencia por consulta, tres consultas en serie cuestan 120 ms. Pero los tres eventos no dependen unos de otros: no hay ninguna razón para esperar a que llegue el primero antes de pedir el segundo.

// BIEN: las tres peticiones salen a la vez.
async function cargarEventosEnParalelo(ids) {
  // map devuelve un array de PROMESAS: las tres operaciones ya han empezado.
  const promesas = ids.map((id) => buscarEventoP(id));

  // Promise.all espera a que TODAS se cumplan.
  return Promise.all(promesas);
}

const inicio2 = Date.now();
cargarEventosEnParalelo(['evt-001', 'evt-002', 'evt-003']).then((eventos) => {
  console.log(`En paralelo: ${eventos.length} eventos en ${Date.now() - inicio2} ms`);
});
En paralelo: 3 eventos en 42 ms

Tres veces más rápido, y con tres eventos. Con treinta, la diferencia sería de 1,2 segundos frente a 40 milisegundos.

gantt
    dateFormat SSS
    axisFormat %L ms
    title Tres consultas de 40 ms
    section En serie (await en bucle)
    evt-001 :a1, 000, 40ms
    evt-002 :a2, after a1, 40ms
    evt-003 :a3, after a2, 40ms
    section En paralelo (Promise.all)
    evt-001 :b1, 000, 40ms
    evt-002 :b2, 000, 40ms
    evt-003 :b3, 000, 40ms

La clave para entenderlo: una promesa empieza a trabajar en el momento en que se crea, no cuando se le hace await. El map crea las tres promesas de golpe, así que las tres operaciones arrancan a la vez; Promise.all solo se encarga de esperar.

Cuándo cada una

Situación Qué usar
El paso B necesita el resultado del paso A await secuencial. No hay alternativa
Los pasos son independientes entre sí Promise.all
Son independientes pero el destino no aguanta la carga (una API con límite) Paralelismo acotado: lotes de N
Quieres el primero que responda Promise.race o Promise.any

En el flujo de compra del apartado 6, los await sí deben ser secuenciales: no puedes emitir entradas de un pedido que no has cobrado. En cambio, cargar el catálogo completo o consultar tres organizadores distintos es trabajo paralelo.

Paralelismo acotado

Lanzar 3.000 peticiones a la vez con Promise.all no es "más rápido": es una forma de tumbar la base de datos o de que la API externa te bloquee. El patrón correcto es procesar en lotes:

// src/utiles/en-lotes.js
// Ejecuta una tarea sobre muchos elementos, como maximo N a la vez.

async function enLotes(elementos, tamanoLote, tarea) {
  const resultados = [];

  for (let i = 0; i < elementos.length; i += tamanoLote) {
    const lote = elementos.slice(i, i + tamanoLote);
    // Dentro del lote, en paralelo. Entre lotes, en serie.
    const resultadosLote = await Promise.all(lote.map(tarea));
    resultados.push(...resultadosLote);
  }

  return resultados;
}

module.exports = { enLotes };
// Consultar 3000 sesiones de 20 en 20.
const ocupaciones = await enLotes(idsSesiones, 20, (id) => consultarOcupacion(id));

  1. Promise.all, allSettled, race y any

Los cuatro combinadores de promesas. Elegir mal es una fuente habitual de errores sutiles.

Método Se cumple cuando… Se rechaza cuando… Devuelve
Promise.all Todas se cumplen Alguna se rechaza (la primera) Array de valores, en el orden de entrada
Promise.allSettled Todas se asientan (nunca se rechaza) Nunca Array de { status, value } o { status, reason }
Promise.race La primera en asentarse se cumple La primera en asentarse se rechaza El valor o error de la primera
Promise.any La primera en cumplirse Todas se rechazan El valor de la primera que funcionó

Promise.all: todo o nada

const [almendra, boveda, ribera] = await Promise.all([
  buscarEventoP('evt-001'),
  buscarEventoP('evt-002'),
  buscarEventoP('evt-003')
]);

La desestructuración funciona porque el orden del resultado es el de entrada, no el de finalización.

Su comportamiento ante el fallo es el que hay que entender bien:

try {
  const eventos = await Promise.all([
    buscarEventoP('evt-001'),
    buscarEventoP('evt-999'),   // No existe: se rechaza
    buscarEventoP('evt-003')
  ]);
} catch (error) {
  console.error(error.message);   // Evento no encontrado: evt-999
  // Y de evt-001 y evt-003 no sabemos nada, aunque probablemente fueron bien.
}

Promise.all es "todo o nada": al primer rechazo, la promesa combinada se rechaza y pierdes los resultados de las demás. Además, las otras operaciones no se cancelan: siguen ejecutándose, simplemente ya no interesan a nadie.

Úsalo cuando necesitas todos los resultados para continuar. Si falta uno, el trabajo no tiene sentido.

Promise.allSettled: quiero saberlo todo

// src/laboratorio/informe-resiliente.js
// Un informe que no debe caerse porque falle un evento.

const resultados = await Promise.allSettled([
  buscarEventoP('evt-001'),
  buscarEventoP('evt-999'),
  buscarEventoP('evt-003')
]);

const encontrados = [];
const fallidos = [];

for (const resultado of resultados) {
  if (resultado.status === 'fulfilled') {
    encontrados.push(resultado.value.titulo);
  } else {
    fallidos.push(resultado.reason.message);
  }
}

console.log(`Encontrados (${encontrados.length}): ${encontrados.join(', ')}`);
console.error(`Fallidos (${fallidos.length}): ${fallidos.join(' | ')}`);
Encontrados (2): Concierto de Otono, Festival de Jazz de Primavera
Fallidos (1): Evento no encontrado: evt-999

allSettled nunca se rechaza. Es la elección correcta para informes, sincronizaciones y cualquier proceso por lotes donde un fallo parcial no debe invalidar el resto. En Escena Viva: enviar 500 correos de confirmación con Promise.all significa que un correo rebotado cancela el informe de los otros 499; con allSettled, sabes exactamente cuáles se enviaron y cuáles no.

Promise.race: el primero que llegue, para bien o para mal

// Tiempo limite: o responde la pasarela, o se rechaza a los 3 segundos.
const pedidoPagado = await Promise.race([
  cobrarPedido(pedido),
  rechazarTras(3000, 'La pasarela de pago no responde')
]);

Su uso principal es exactamente ese: imponer un tiempo límite. Lo desarrollamos en el apartado 11.

Cuidado con una trampa: race se asienta con la primera promesa que se asiente, incluso si es un rechazo. Si quieres "el primero que funcione", ignorando fallos, necesitas any.

Promise.any: el primero que funcione

// Consultar el precio a tres proveedores; nos vale el primero que responda bien.
try {
  const tipoCambio = await Promise.any([
    consultarProveedorA(),
    consultarProveedorB(),
    consultarProveedorC()
  ]);
  console.log(`Tipo de cambio obtenido: ${tipoCambio}`);
} catch (error) {
  // AggregateError: contiene TODOS los errores en error.errors
  console.error(`Ningun proveedor respondio (${error.errors.length} fallos)`);
  for (const fallo of error.errors) {
    console.error(`  - ${fallo.message}`);
  }
}

Promise.any solo se rechaza si todas fallan, y lo hace con un AggregateError que contiene el array completo de errores en .errors. Es el patrón de redundancia: varias réplicas o varios proveedores para lo mismo.

Guía rápida de decisión

Quiero… Uso
Cargar los datos que necesito para responder, y si falta uno no puedo responder Promise.all
Procesar un lote donde los fallos parciales son aceptables y hay que reportarlos Promise.allSettled
Poner un tiempo límite a una operación Promise.race
Consultar varias fuentes redundantes y quedarme con la primera que funcione Promise.any

  1. Propagación de errores y rechazos no manejados

9.1 Los errores suben por la pila async

async function nivel3() {
  throw new Error('fallo en el nivel mas profundo');
}

async function nivel2() {
  await nivel3();          // El rechazo se propaga hacia arriba
  console.log('Esto no se ejecuta');
}

async function nivel1() {
  try {
    await nivel2();
  } catch (error) {
    console.error(`Capturado en nivel1: ${error.message}`);
  }
}

nivel1();   // Capturado en nivel1: fallo en el nivel mas profundo

Exactamente igual que con código síncrono. Es la propiedad que hace que async/await sea tan cómodo: capturas los errores donde tienes contexto para decidir qué hacer, no en cada paso intermedio.

9.2 El rechazo no manejado tumba el proceso

Si una promesa se rechaza y nadie ha registrado un .catch ni la ha esperado dentro de un try, se produce un rechazo no manejado (unhandled rejection).

// src/laboratorio/rechazo-no-manejado.js

async function cobrar() {
  throw new Error('La pasarela devolvio un error 500');
}

cobrar();   // Se llama, se ignora el resultado. NADIE captura el rechazo.

console.log('El script continua...');
El script continua...

node:internal/process/promises:288
            triggerUncaughtException(err, true /* fromPromise */);
            ^
Error: La pasarela devolvio un error 500
    ...
[el proceso muere con codigo 1]

Desde Node.js 15, un rechazo no manejado termina el proceso. Antes solo imprimía un aviso, y muchas aplicaciones vivían con decenas de rechazos silenciosos que ocultaban errores reales. El cambio fue deliberado: un rechazo sin manejar es un error de programación, exactamente igual que una excepción sin capturar.

Los tres descuidos que lo provocan:

// 1. Llamar a una funcion async sin await ni .catch
procesarPedido(pedido);                       // MAL
await procesarPedido(pedido);                 // BIEN
procesarPedido(pedido).catch(registrarError); // BIEN, si no quieres esperar

// 2. Olvidar el catch en una cadena
buscarEventoP(id).then((e) => console.log(e.titulo));            // MAL
buscarEventoP(id).then((e) => console.log(e.titulo)).catch(log); // BIEN

// 3. La funcion principal sin proteger
async function principal() { /* ... */ }
principal();                                   // MAL
principal().catch((error) => {                 // BIEN
  console.error(`Fallo fatal: ${error.message}`);
  process.exitCode = 1;
});

Como red de seguridad para el registro —no como manejo de errores— puedes escuchar el evento del proceso:

// src/utiles/red-de-seguridad.js
// Registra cualquier rechazo no manejado antes de que el proceso muera.

process.on('unhandledRejection', (motivo, promesa) => {
  console.error('RECHAZO NO MANEJADO. Esto es un fallo de programacion.');
  console.error(motivo instanceof Error ? motivo.stack : motivo);
  process.exitCode = 1;
});

Esto sirve para enterarte del problema y registrarlo en producción, no para seguir como si nada. Lo formalizaremos en el Módulo 11.

  1. await de nivel superior

En un módulo CommonJS —el que estamos usando y el que estudiaremos en Módulos CommonJS y require()— await solo puede aparecer dentro de una función async. Por eso hemos tenido que envolver todo en una función principal().

En un módulo ES, await funciona directamente en el nivel superior del fichero:

// src/laboratorio/carga.mjs   (fijate en la extension .mjs)
import { readFile } from 'node:fs/promises';

// await directamente, sin envolver en ninguna funcion.
const contenido = await readFile('datos/eventos.json', 'utf8');
const catalogo = JSON.parse(contenido);

console.log(`Cargados ${catalogo.length} eventos`);

Es una de las ventajas exclusivas de los módulos ES, y es especialmente cómoda para scripts, para inicialización de configuración y para el REPL (que, como viste en el Módulo 1, también lo admite).

Lo veremos en detalle —junto con cómo activarlo, sus reglas y su interoperabilidad con CommonJS— en la lección Módulos ES e Interoperabilidad.

  1. Patrones útiles: dormir, reintentos y tiempo límite

Tres utilidades que escribirás una vez y usarás siempre.

11.1 dormir(ms)

// src/utiles/dormir.js
function dormir(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

module.exports = { dormir };

Node trae una versión nativa desde la versión 15, en timers/promises:

const { setTimeout: dormir } = require('node:timers/promises');

await dormir(1000);
console.log('Un segundo despues');

Usa la nativa cuando puedas: admite además una señal de cancelación (AbortSignal).

11.2 Reintentos con espera creciente

Las operaciones de red fallan de forma transitoria: un pico de latencia, un reinicio del servicio, un límite de peticiones momentáneo. Reintentar es a menudo la respuesta correcta.

// src/utiles/reintentar.js
// Reintenta una operacion asincrona con espera exponencial.

const { setTimeout: dormir } = require('node:timers/promises');

async function reintentar(operacion, opciones = {}) {
  const {
    intentos = 3,
    esperaInicialMs = 200,
    factor = 2,
    esReintentable = () => true
  } = opciones;

  let ultimoError;

  for (let intento = 1; intento <= intentos; intento++) {
    try {
      return await operacion(intento);
    } catch (error) {
      ultimoError = error;

      // Un error de negocio no se reintenta: reintentar un aforo
      // insuficiente no lo va a arreglar.
      if (!esReintentable(error) || intento === intentos) {
        throw error;
      }

      // Espera exponencial: 200, 400, 800 ms...
      const espera = esperaInicialMs * factor ** (intento - 1);
      console.error(
        `[reintento] intento ${intento}/${intentos} fallido (${error.message}). ` +
        `Reintentando en ${espera} ms.`
      );
      await dormir(espera);
    }
  }

  throw ultimoError;
}

module.exports = { reintentar };
// Uso en Escena Viva: cobrar reintentando solo los fallos de red.
const CODIGOS_REINTENTABLES = new Set(['ETIMEDOUT', 'ECONNRESET', 'PASARELA_NO_DISPONIBLE']);

const pedidoPagado = await reintentar(
  () => cobrarPedido(pedido),
  {
    intentos: 4,
    esperaInicialMs: 250,
    esReintentable: (error) => CODIGOS_REINTENTABLES.has(error.codigo)
  }
);

Fíjate en esReintentable. Es la parte más importante del patrón, y la que casi todo el mundo omite: reintentar un PAGO_RECHAZADO cuatro veces no solo es inútil, sino que puede duplicar cargos. Solo se reintenta lo que es transitorio.

En producción se añade además jitter: una pequeña variación aleatoria en la espera, para que mil clientes que fallaron a la vez no reintenten todos en el mismo milisegundo.

11.3 Tiempo límite con Promise.race

Una operación asíncrona que nunca responde es peor que una que falla: deja recursos ocupados indefinidamente.

// src/utiles/con-limite-de-tiempo.js
// Envuelve una promesa con un tiempo maximo de espera.

function conLimiteDeTiempo(promesa, limiteMs, mensaje = 'Tiempo de espera agotado') {
  let temporizador;

  const limite = new Promise((_, reject) => {
    temporizador = setTimeout(() => {
      const error = new Error(`${mensaje} (${limiteMs} ms)`);
      error.codigo = 'TIEMPO_AGOTADO';
      reject(error);
    }, limiteMs);
  });

  // El primero que se asiente gana.
  return Promise.race([promesa, limite]).finally(() => {
    // Limpiamos el temporizador para no retener el proceso vivo.
    clearTimeout(temporizador);
  });
}

module.exports = { conLimiteDeTiempo };
try {
  const pedidoPagado = await conLimiteDeTiempo(
    cobrarPedido(pedido),
    3000,
    'La pasarela de pago no responde'
  );
  console.log(`Cobrado: ${pedidoPagado.id}`);
} catch (error) {
  if (error.codigo === 'TIEMPO_AGOTADO') {
    console.error('Reintentar mas tarde o avisar al usuario.');
  }
  throw error;
}

Ese .finally(() => clearTimeout(temporizador)) no es un detalle menor: sin él, el temporizador mantiene vivo el proceso hasta que venza, aunque la operación haya terminado en 20 ms. Es justo el mecanismo de cuenta de referencias que viste en la lección de arquitectura.

Y una advertencia honesta: el tiempo límite no cancela la operación subyacente. La petición HTTP sigue en marcha; simplemente dejas de esperarla. Para cancelar de verdad hace falta AbortController, que las APIs modernas de Node (fetch, fs/promises, timers/promises) sí aceptan.

  1. Tabla resumen: callbacks, promesas y async/await

Criterio Callbacks Promesas (.then) async/await
Sintaxis Funciones anidadas Cadena de métodos Lineal, como código síncrono
Legibilidad de flujos largos Mala (pirámide) Buena Excelente
Manejo de errores if (error) en cada paso Un .catch por cadena try/catch/finally nativo
finally / limpieza Manual .finally() finally nativo
Devolver un valor Imposible Sí (una promesa) Sí, con return
Ejecución en paralelo Manual y propensa a errores Promise.all Promise.all + await
Se puede llamar dos veces Sí, es un bug frecuente Imposible Imposible
Variables entre pasos Cierres anidados o contexto Cierres o encadenado Variables locales normales
Depuración y trazas Trazas pobres Mejores Trazas async completas
Múltiples resultados a lo largo del tiempo Sí No (se asienta una vez) No
Coste de rendimiento El menor Cola de microtareas Como las promesas
Soporte en Node Siempre Desde Node 0.12 Desde Node 7.6
Cuándo usarlo Eventos, streams, APIs antiguas Composición puntual, Promise.all Todo lo demás

La conclusión práctica para el resto del curso:

Escribe async/await por defecto. Usa .then cuando compongas promesas sin necesitar esperarlas (Promise.all, .catch en una llamada que no esperas). Usa callbacks cuando la API te obligue o cuando el resultado sea repetido en el tiempo — es decir, cuando estés ante un evento.

Errores Comunes y Consejos

Error 1: olvidar el return dentro de un .then. La cadena no espera y el siguiente paso recibe undefined. Usa flechas sin llaves para evitarlo.

Error 2: await dentro de un for cuando las tareas son independientes. Multiplicas el tiempo por el número de elementos. Promise.all con map.

Error 3: creer que await bloquea el proceso. Solo pausa la función que lo contiene. El bucle de eventos sigue girando.

Error 4: llamar a una función async sin await ni .catch. Rechazo no manejado, y desde Node 15 eso mata el proceso.

Error 5: Promise.all cuando lo correcto es allSettled. Un fallo parcial cancela todo el lote y pierdes los resultados buenos.

Error 6: forEach con funciones async.

// NO espera a nada: forEach ignora las promesas que devuelve el callback.
ids.forEach(async (id) => { await procesar(id); });
console.log('Terminado');   // Mentira: no ha terminado nada

// Correcto:
await Promise.all(ids.map((id) => procesar(id)));

Error 7: envolver una promesa en new Promise. El constructor antipattern. Si ya es una promesa, devuélvela tal cual.

Error 8: reject('texto') en lugar de reject(new Error('texto')). Pierdes la traza de pila y rompes la convención que todo el ecosistema espera.

Error 9: reintentar errores que no son transitorios. Reintentar cuatro veces un pago rechazado puede acabar en cargos duplicados.

Consejo 1: protege siempre tu función principal. principal().catch(...) en la última línea del fichero, sin excepción.

Consejo 2: pregúntate en cada await: "¿esto necesita el resultado del anterior?" Si la respuesta es no, hay una oportunidad de Promise.all.

Consejo 3: usa node:fs/promises y node:timers/promises directamente. No promisifiques lo que Node ya te da hecho.

Consejo 4: mantén los errores con codigo. La convención de la lección anterior sigue valiendo, y con promesas es todavía más útil, porque un solo catch recibe errores de pasos muy distintos y necesita distinguirlos.

Ejercicios

Ejercicio 1: convertir la capa de datos y medir la diferencia

Escribe src/laboratorio/catalogo-promesas.js que:

  1. Tome las funciones buscarEvento, buscarSesion y reservarEntradas de la lección anterior y las convierta con util.promisify.
  2. Implemente cargarCatalogoSerie(ids) con await en un bucle for.
  3. Implemente cargarCatalogoParalelo(ids) con Promise.all.
  4. Ejecute las dos con los tres eventos, mida los tiempos con Date.now() y muestre una tabla comparativa con console.table que incluya el tiempo de cada una y el factor de mejora.
  5. Añada una tercera variante cargarCatalogoResiliente(ids) con Promise.allSettled que funcione incluso si se le pasa evt-999, informando de los fallos por stderr.

Responde por escrito: ¿qué factor de mejora obtienes con 3 eventos? ¿Y con 10? ¿Por qué el factor no crece indefinidamente en un caso real?

Ejercicio 2: la compra con tiempo límite y reintentos

Partiendo del flujo comprarEntradas del apartado 6, escribe src/laboratorio/compra-robusta.js que añada:

  1. Un tiempo límite de 2 segundos al cobro, usando conLimiteDeTiempo.
  2. Reintentos del cobro: hasta 3 intentos con espera exponencial desde 200 ms, solo para errores con código TIEMPO_AGOTADO, ETIMEDOUT o PASARELA_NO_DISPONIBLE.
  3. Una pasarela simulada cobrarPedido(pedido) que:
    • Rechace con PAGO_RECHAZADO si el total supera 20000 céntimos (no reintentable).
    • Falle con PASARELA_NO_DISPONIBLE las dos primeras veces que se la llame y funcione a la tercera (reintentable).
  4. Compensación correcta con try/catch: si algo falla tras reservar el aforo, se libera.
  5. Registro por stderr de cada intento, y resultado final por stdout.

Comprueba los dos caminos: uno que acaba funcionando tras dos reintentos y uno que se rechaza sin reintentar.

Ejercicio 3: el panel de ocupación, en paralelo y por lotes

Escribe src/laboratorio/panel-ocupacion.js que genere el panel de ocupación de Escena Viva:

  1. Una función consultarOcupacion(sesionId) que devuelva una promesa con { sesionId, sala, porcentaje, recaudacionCentimos } tras una latencia de 30 ms.
  2. generarPanel(idsSesiones, tamanoLote) que use el ayudante enLotes del apartado 7 para consultar todas las sesiones con un máximo de tamanoLote en vuelo simultáneamente.
  3. Agregación por sala: total de sesiones, ocupación media y recaudación en euros.
  4. Medición del retraso del bucle de eventos durante la generación, con monitorEventLoopDelay de la lección anterior, para demostrar que la versión con promesas no bloquea aunque procese 300 sesiones.
  5. Comparativa de tiempos con tamaños de lote 1, 10, 50 y 300.

Responde: ¿por qué el lote de 300 es el más rápido aquí y aun así no sería la mejor elección contra una base de datos real?

Soluciones

Solución 1

// src/laboratorio/catalogo-promesas.js
// Compara la carga secuencial, paralela y resiliente del catalogo.

const { promisify } = require('node:util');

// buscarEvento viene de la leccion anterior (callback error-first).
const buscarEventoP = promisify(buscarEvento);

// 2. En serie: cada peticion espera a la anterior.
async function cargarCatalogoSerie(ids) {
  const eventos = [];
  for (const id of ids) {
    eventos.push(await buscarEventoP(id));
  }
  return eventos;
}

// 3. En paralelo: todas las peticiones arrancan a la vez.
async function cargarCatalogoParalelo(ids) {
  return Promise.all(ids.map((id) => buscarEventoP(id)));
}

// 5. Resiliente: los fallos parciales no invalidan el resto.
async function cargarCatalogoResiliente(ids) {
  const resultados = await Promise.allSettled(ids.map((id) => buscarEventoP(id)));

  const eventos = [];
  const fallos = [];

  resultados.forEach((resultado, indice) => {
    if (resultado.status === 'fulfilled') {
      eventos.push(resultado.value);
    } else {
      fallos.push({ id: ids[indice], motivo: resultado.reason.message });
    }
  });

  for (const fallo of fallos) {
    console.error(`[catalogo] no se pudo cargar ${fallo.id}: ${fallo.motivo}`);
  }

  return { eventos, fallos };
}

// --- Medicion ---
async function medir(etiqueta, funcion, ids) {
  const inicio = Date.now();
  const resultado = await funcion(ids);
  const ms = Date.now() - inicio;
  const cuantos = Array.isArray(resultado) ? resultado.length : resultado.eventos.length;
  return { etiqueta, eventos: cuantos, ms };
}

async function principal() {
  const ids = ['evt-001', 'evt-002', 'evt-003'];

  const serie = await medir('serie', cargarCatalogoSerie, ids);
  const paralelo = await medir('paralelo', cargarCatalogoParalelo, ids);

  console.table([
    serie,
    paralelo,
    { etiqueta: 'mejora', eventos: '-', ms: `x${(serie.ms / paralelo.ms).toFixed(1)}` }
  ]);

  console.error('');
  console.error('--- Variante resiliente con un id inexistente ---');
  const resiliente = await cargarCatalogoResiliente([...ids, 'evt-999']);
  console.log(
    `Resiliente: ${resiliente.eventos.length} cargados, ${resiliente.fallos.length} fallidos`
  );
}

principal().catch((error) => {
  console.error(`Fallo fatal: ${error.message}`);
  process.exitCode = 1;
});
┌─────────┬────────────┬─────────┬──────┐
│ (index) │ etiqueta   │ eventos │ ms   │
├─────────┼────────────┼─────────┼──────┤
│ 0       │ 'serie'    │ 3       │ 124  │
│ 1       │ 'paralelo' │ 3       │ 42   │
│ 2       │ 'mejora'   │ '-'     │ 'x3.0' │
└─────────┴────────────┴─────────┴──────┘

--- Variante resiliente con un id inexistente ---
[catalogo] no se pudo cargar evt-999: Evento no encontrado: evt-999
Resiliente: 3 cargados, 1 fallidos

Respuestas:

  • Con 3 eventos: factor ~3. Con 10 eventos: factor ~10 (400 ms frente a 40 ms). En este laboratorio la mejora es lineal porque setTimeout no consume ningún recurso compartido.
  • En un caso real el factor no crece indefinidamente por tres motivos: el destino tiene un límite (una base de datos con 20 conexiones no atiende 500 consultas simultáneas más rápido que 20 a la vez), la red tiene un ancho de banda finito, y el propio Node tiene el thread pool de 4 hilos para las operaciones que pasan por él. A partir de cierto punto, más concurrencia solo añade cola. De ahí el patrón de paralelismo acotado por lotes.

Solución 2

// src/laboratorio/compra-robusta.js
// Flujo de compra con tiempo limite, reintentos selectivos y compensacion.

const { setTimeout: dormir } = require('node:timers/promises');

const CODIGOS_REINTENTABLES = new Set([
  'TIEMPO_AGOTADO', 'ETIMEDOUT', 'PASARELA_NO_DISPONIBLE'
]);

// --- Utilidades ---

function conLimiteDeTiempo(promesa, limiteMs, mensaje = 'Tiempo de espera agotado') {
  let temporizador;
  const limite = new Promise((_, reject) => {
    temporizador = setTimeout(() => {
      const error = new Error(`${mensaje} (${limiteMs} ms)`);
      error.codigo = 'TIEMPO_AGOTADO';
      reject(error);
    }, limiteMs);
  });
  return Promise.race([promesa, limite]).finally(() => clearTimeout(temporizador));
}

async function reintentar(operacion, { intentos = 3, esperaInicialMs = 200, factor = 2 } = {}) {
  for (let intento = 1; intento <= intentos; intento++) {
    try {
      return await operacion(intento);
    } catch (error) {
      const reintentable = CODIGOS_REINTENTABLES.has(error.codigo);

      if (!reintentable) {
        console.error(`[reintento] "${error.codigo}" no es reintentable. Abandono.`);
        throw error;
      }
      if (intento === intentos) {
        console.error(`[reintento] agotados los ${intentos} intentos.`);
        throw error;
      }

      const espera = esperaInicialMs * factor ** (intento - 1);
      console.error(
        `[reintento] intento ${intento}/${intentos} fallido (${error.message}). ` +
        `Reintento en ${espera} ms.`
      );
      await dormir(espera);
    }
  }
}

// --- Pasarela simulada ---

let llamadasAPasarela = 0;

function cobrarPedido(pedido) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // Error de negocio: NO reintentable.
      if (pedido.totalCentimos > 20000) {
        const error = new Error(
          `Pago rechazado: ${(pedido.totalCentimos / 100).toFixed(2)} EUR supera el limite`
        );
        error.codigo = 'PAGO_RECHAZADO';
        reject(error);
        return;
      }

      // Error transitorio: las dos primeras llamadas fallan.
      llamadasAPasarela++;
      if (llamadasAPasarela <= 2) {
        const error = new Error('La pasarela no esta disponible');
        error.codigo = 'PASARELA_NO_DISPONIBLE';
        reject(error);
        return;
      }

      resolve({ ...pedido, estado: 'pagado' });
    }, 40);
  });
}

// --- Flujo de compra ---

async function comprarEntradas(usuarioId, eventoId, sesionId, cantidad) {
  const evento = await buscarEventoP(eventoId);
  await reservarEntradasP(sesionId, cantidad);

  try {
    const { sesion } = await buscarSesionP(sesionId);
    const pedido = await crearPedidoP(usuarioId, sesion, cantidad);

    // Reintentos por fuera, tiempo limite por dentro: cada intento
    // tiene sus propios 2 segundos.
    const pedidoPagado = await reintentar(
      () => conLimiteDeTiempo(cobrarPedido(pedido), 2000, 'La pasarela no responde'),
      { intentos: 3, esperaInicialMs: 200 }
    );

    const entradas = await emitirEntradasP(pedidoPagado);
    return { evento, sesion, pedido: pedidoPagado, entradas };
  } catch (error) {
    const libres = await liberarAforoP(sesionId, cantidad);
    console.error(`[compensacion] aforo liberado en ${sesionId}, quedan ${libres} libres`);
    throw error;
  }
}

// --- Pruebas ---

async function principal() {
  // Camino 1: 3 x 18,00 = 54,00 EUR. Falla dos veces y funciona a la tercera.
  console.error('--- Compra que acaba funcionando tras dos reintentos ---');
  const compra = await comprarEntradas('asis-001', 'evt-002', 'ses-002-2', 3);
  console.log(`Pedido ${compra.pedido.id} - ${compra.pedido.estado}`);
  console.log(`  Importe: ${(compra.pedido.totalCentimos / 100).toFixed(2)} EUR`);
  console.log(`  Entradas: ${compra.entradas.map((e) => e.codigo).join(', ')}`);

  // Camino 2: 5 x 42,00 = 210,00 EUR. Rechazo no reintentable.
  console.error('');
  console.error('--- Compra rechazada sin reintentos ---');
  try {
    await comprarEntradas('asis-002', 'evt-003', 'ses-003-2', 5);
  } catch (error) {
    console.error(`Resultado: [${error.codigo}] ${error.message}`);
  }
}

principal().catch((error) => {
  console.error(`Fallo fatal: ${error.message}`);
  process.exitCode = 1;
});
--- Compra que acaba funcionando tras dos reintentos ---
[reintento] intento 1/3 fallido (La pasarela no esta disponible). Reintento en 200 ms.
[reintento] intento 2/3 fallido (La pasarela no esta disponible). Reintento en 400 ms.
Pedido ped-001 - pagado
  Importe: 54.00 EUR
  Entradas: EV-2026-000001, EV-2026-000002, EV-2026-000003

--- Compra rechazada sin reintentos ---
[reintento] "PAGO_RECHAZADO" no es reintentable. Abandono.
[compensacion] aforo liberado en ses-003-2, quedan 180 libres
Resultado: [PAGO_RECHAZADO] Pago rechazado: 210.00 EUR supera el limite

Dos decisiones de diseño que conviene subrayar:

  1. El tiempo límite va dentro del reintento, no fuera. Así cada intento dispone de sus propios 2 segundos. Al revés, el límite global cortaría la cadena de reintentos a medias.
  2. PAGO_RECHAZADO se abandona al primer intento. Es la línea que separa un reintento útil de un cobro duplicado.

Solución 3

// src/laboratorio/panel-ocupacion.js
// Panel de ocupacion de Escena Viva con paralelismo acotado y medicion del bucle.

const { monitorEventLoopDelay } = require('node:perf_hooks');

const SALAS = ['Teatro Almendra', 'Sala Boveda', 'Auditorio Ribera'];
const LATENCIA_MS = 30;

// 1. Consulta simulada de una sesion.
function consultarOcupacion(sesionId, indice) {
  return new Promise((resolve) => {
    setTimeout(() => {
      const aforo = 420;
      const vendidas = (indice * 37) % aforo;
      resolve({
        sesionId,
        sala: SALAS[indice % SALAS.length],
        porcentaje: Math.round((vendidas / aforo) * 100),
        recaudacionCentimos: vendidas * 2500
      });
    }, LATENCIA_MS);
  });
}

// 2. Paralelismo acotado.
async function enLotes(elementos, tamanoLote, tarea) {
  const resultados = [];
  for (let i = 0; i < elementos.length; i += tamanoLote) {
    const lote = elementos.slice(i, i + tamanoLote);
    resultados.push(...await Promise.all(lote.map(tarea)));
  }
  return resultados;
}

async function generarPanel(idsSesiones, tamanoLote) {
  return enLotes(idsSesiones, tamanoLote, (id, i) => consultarOcupacion(id, i));
}

// 3. Agregacion por sala.
function agregarPorSala(ocupaciones) {
  const porSala = new Map();

  for (const o of ocupaciones) {
    const a = porSala.get(o.sala) ?? { sala: o.sala, sesiones: 0, sumaPorcentajes: 0, recaudacionCentimos: 0 };
    a.sesiones++;
    a.sumaPorcentajes += o.porcentaje;
    a.recaudacionCentimos += o.recaudacionCentimos;
    porSala.set(o.sala, a);
  }

  return [...porSala.values()].map((a) => ({
    sala: a.sala,
    sesiones: a.sesiones,
    ocupacionMedia: `${Math.round(a.sumaPorcentajes / a.sesiones)}%`,
    recaudacionEuros: (a.recaudacionCentimos / 100).toFixed(2)
  }));
}

async function principal() {
  const ids = Array.from({ length: 300 }, (_, i) => `ses-${String(i + 1).padStart(3, '0')}-1`);

  // 4. Medicion del retraso del bucle durante todo el proceso.
  const histograma = monitorEventLoopDelay({ resolution: 5 });
  histograma.enable();

  // 5. Comparativa de tamanos de lote.
  const comparativa = [];
  for (const tamanoLote of [1, 10, 50, 300]) {
    const inicio = Date.now();
    await generarPanel(ids, tamanoLote);
    comparativa.push({ tamanoLote, ms: Date.now() - inicio });
  }

  histograma.disable();

  const ocupaciones = await generarPanel(ids, 50);

  console.log('PANEL DE OCUPACION');
  console.table(agregarPorSala(ocupaciones));

  console.log('');
  console.log('TIEMPO SEGUN EL TAMANO DE LOTE');
  console.table(comparativa);

  const aMs = (n) => (n / 1e6).toFixed(2);
  console.error('');
  console.error(`Retraso del bucle: media ${aMs(histograma.mean)} ms, p99 ${aMs(histograma.percentile(99))} ms`);
}

principal().catch((error) => {
  console.error(`Fallo fatal: ${error.message}`);
  process.exitCode = 1;
});
PANEL DE OCUPACION
┌─────────┬───────────────────┬──────────┬────────────────┬──────────────────┐
│ (index) │ sala              │ sesiones │ ocupacionMedia │ recaudacionEuros │
├─────────┼───────────────────┼──────────┼────────────────┼──────────────────┤
│ 0       │ 'Teatro Almendra' │ 100      │ '49%'          │ '515450.00'      │
│ 1       │ 'Sala Boveda'     │ 100      │ '50%'          │ '523900.00'      │
│ 2       │ 'Auditorio Ribera'│ 100      │ '50%'          │ '521100.00'      │
└─────────┴───────────────────┴──────────┴────────────────┴──────────────────┘

TIEMPO SEGUN EL TAMANO DE LOTE
┌─────────┬────────────┬──────┐
│ (index) │ tamanoLote │ ms   │
├─────────┼────────────┼──────┤
│ 0       │ 1          │ 9412 │
│ 1       │ 10         │ 942  │
│ 2       │ 50         │ 192  │
│ 3       │ 300        │ 32   │
└─────────┴────────────┴──────┘

Retraso del bucle: media 5.31 ms, p99 11.08 ms

Análisis:

  • El lote de 300 es el más rápido (32 ms frente a 9,4 segundos) porque las 300 esperas transcurren simultáneamente: el coste total es el de una sola latencia de 30 ms.
  • El retraso del bucle se mantiene en 5-11 ms incluso con 300 operaciones en vuelo. Compáralo con el informe síncrono de la lección anterior, que disparaba el p99 a 414 ms. Esta es la demostración de que la asincronía bien usada escala sin bloquear.
  • Y sin embargo el lote de 300 no sería la elección correcta contra un sistema real: una base de datos con un grupo de 20 conexiones no ejecuta 300 consultas simultáneas, las encola; una API externa con límite de 100 peticiones por minuto te devolvería errores 429; y 300 respuestas llegando a la vez multiplican el uso de memoria. El tamaño de lote se elige por lo que aguanta el destino, no por lo que aguanta Node. Un valor entre 10 y 50 es el punto habitual, y se ajusta midiendo.

Conclusión

Has recuperado el lenguaje. Una promesa es un objeto con tres estados —pendiente, cumplida, rechazada—, que cambia de estado exactamente una vez y cuyo resultado es inmutable; esa sola propiedad elimina por construcción el bug de "el callback se llamó dos veces". La consumes con .then, .catch y .finally, y como cada uno devuelve una promesa nueva, los pasos se encadenan planos con un único punto de tratamiento de errores en lugar de la pirámide de la lección anterior.

Has aprendido a crearlas: new Promise(resolve, reject) para envolver lo que aún no es una promesa —un temporizador, un evento, una pasarela de pago— y, sobre todo, util.promisify para convertir de un plumazo las funciones error-first de Escena Viva que escribiste ayer. Y has visto el antipatrón que hay que evitar: nunca envolver una promesa dentro de otra.

Sobre esa base, async/await con sus tres reglas: una función async siempre devuelve una promesa, await pausa solo la función que lo contiene —lo demostraste con un latido que seguía sonando durante la espera— y try/catch/finally vuelve a funcionar. El flujo de compra completo pasó de 70 líneas con objeto contexto y dos rutas de error a 30 líneas con variables locales normales, un solo catch y una compensación de dos líneas.

Has interiorizado la distinción que separa el código lento del rápido: await en un bucle for sobre tareas independientes multiplica el tiempo por el número de elementos, mientras que Promise.all sobre un map lo deja en el coste de la más lenta. Y sabes elegir entre los cuatro combinadores: all cuando lo necesitas todo, allSettled cuando los fallos parciales son tolerables y hay que reportarlos, race para imponer un tiempo límite y any para fuentes redundantes. También sabes que el paralelismo se acota por lo que aguanta el destino, no por lo que aguanta Node.

Por último, tienes las herramientas de robustez que usarás siempre: dormir (o node:timers/promises), reintentos con espera exponencial y un predicado esReintentable que evita reintentar un pago rechazado, y tiempos límite con Promise.race recordando limpiar el temporizador en el finally. Y sabes que un rechazo no manejado mata el proceso desde Node 15, así que tu función principal siempre lleva su .catch.

Queda un caso que ni las promesas ni async/await cubren, y no por casualidad: una promesa se asienta una sola vez, pero hay resultados que ocurren muchas veces a lo largo del tiempo. Una sesión que se agota, una venta que se registra, un aforo que baja del 10 %, un socket que recibe datos: eso no es "un resultado futuro", es un flujo de sucesos. Para eso Node tiene un mecanismo propio que lleva en su ADN desde el primer día. En la siguiente lección, Eventos y EventEmitter, construirás el GestorDeVentas de Escena Viva y harás que el sistema entero se entere, sin acoplamiento alguno, en el instante en que una sesión se queda sin entradas.

Curso de Node.js: De Principiante a Avanzado

Módulo 1: Introducción a Node.js

Módulo 2: Conceptos Básicos

Módulo 3: Sistema de Archivos y E/S

Módulo 4: HTTP y Servidores Web

Módulo 5: NPM y Gestión de Paquetes

Módulo 6: Framework Express.js

Módulo 7: Bases de Datos y ORMs

Módulo 8: Autenticación y Autorización

Módulo 9: Pruebas y Depuración

Módulo 10: Temas Avanzados

Módulo 11: Despliegue y DevOps

Módulo 12: Proyectos del Mundo Real

© Copyright 2026. Todos los derechos reservados