Ya sabes cómo gira el bucle de eventos. Lo que aún no has visto es cómo se le entrega trabajo. La respuesta, en la Node.js clásica y todavía hoy en buena parte de su API, es el callback: una función que tú escribes y que Node guarda para llamarla más tarde, cuando el trabajo esté listo.
Esta lección enseña el patrón desde cero. No es historia antigua: aunque en el día a día escribas async/await, los callbacks siguen estando por debajo de todo —los usan los streams, los EventEmitter, el servidor HTTP y decenas de APIs de Node que nunca han recibido una versión con promesas—, y entender sus reglas es la única forma de entender qué resuelven exactamente las promesas de la lección siguiente.
Vas a aprender la forma canónica del callback en Node (el error-first callback), vas a escribir tus propias funciones asíncronas para Escena Viva, vas a descubrir por qué try/catch deja de funcionar en cuanto hay asincronía de por medio, y vas a construir con tus manos —a propósito— la temida pirámide de la muerte. Sentir esa incomodidad en carne propia es la mejor preparación posible para la lección siguiente.
Contenido
- Qué es un callback
- Síncrono frente a asíncrono: el mismo problema, dos formas
- El patrón canónico de Node: el error-first callback
- Escribir tus propias funciones asíncronas
- La regla de oro: exactamente una vez, y siempre asíncronamente
- Por qué
try/catchno captura los errores asíncronos - La pirámide de la muerte en Escena Viva
- Técnicas clásicas de mitigación
- Ventajas e inconvenientes de los callbacks
- Por qué los callbacks siguen importando
- Qué es un callback
Un callback (función de retorno) es simplemente una función que se pasa como argumento a otra función para que esta la llame en algún momento. No es un concepto de Node ni de asincronía: es una consecuencia de que en JavaScript las funciones son valores como los números o las cadenas.
De hecho llevas usando callbacks desde el Módulo 1 sin darles ese nombre:
// Callbacks SINCRONOS: map, filter y sort llaman a tu funcion
// inmediatamente, tantas veces como haga falta, y devuelven el resultado.
const titulos = catalogo.map((evento) => evento.titulo);
const grandes = catalogo.filter((evento) => evento.sala === 'Auditorio Ribera');
const ordenados = [...catalogo].sort((a, b) => a.titulo.localeCompare(b.titulo));Estos son callbacks síncronos: cuando map devuelve, tu función ya se ha ejecutado todas las veces necesarias. No hay bucle de eventos involucrado.
Los interesantes para nosotros son los asíncronos:
// Callback ASINCRONO: se registra ahora y se ejecuta despues,
// cuando el bucle de eventos llegue a la fase correspondiente.
setTimeout(() => {
console.log('Esto se ejecuta despues');
}, 1000);
console.log('Esto se ejecuta antes');La diferencia es fundamental y define toda la lección:
| Callback síncrono | Callback asíncrono | |
|---|---|---|
| Cuándo se ejecuta | Antes de que la función que lo recibe devuelva | Después, desde el bucle de eventos |
| Ejemplos | map, filter, reduce, sort, forEach |
setTimeout, fs.readFile, servidor.on('request') |
¿Se puede envolver en try/catch? |
Sí | No (apartado 6) |
| ¿Puede devolver un valor útil? | Sí, al que lo llamó | No: quien lo llamó ya devolvió hace rato |
- Síncrono frente a asíncrono: el mismo problema, dos formas
Planteemos un problema concreto de Escena Viva: buscar un evento por su identificador. Hoy los datos están en memoria, pero en el Módulo 3 estarán en datos/eventos.json y en el Módulo 7 en una base de datos. Es decir: hoy es instantáneo, mañana implicará esperar.
Versión síncrona, la que sabrías escribir sin este curso:
// src/laboratorio/buscar-sincrono.js
const catalogo = [
{ id: 'evt-001', titulo: 'Concierto de Otono', sala: 'Teatro Almendra' },
{ id: 'evt-002', titulo: 'Noche de Monologos', sala: 'Sala Boveda' },
{ id: 'evt-003', titulo: 'Festival de Jazz de Primavera', sala: 'Auditorio Ribera' }
];
function buscarEventoSincrono(id) {
const evento = catalogo.find((e) => e.id === id);
if (!evento) {
throw new Error(`Evento no encontrado: ${id}`);
}
return evento;
}
// Uso: natural, lineal, con try/catch que funciona.
try {
const evento = buscarEventoSincrono('evt-002');
console.log(`Encontrado: ${evento.titulo}`);
} catch (error) {
console.error(`Error: ${error.message}`);
}Todo encaja: el valor se devuelve, el error se lanza, y try/catch lo recoge. Es el modelo mental con el que aprendiste a programar.
Ahora la versión asíncrona, que es la que necesitarás en cuanto los datos vengan de disco o de red:
// src/laboratorio/buscar-asincrono.js
const catalogo = [
{ id: 'evt-001', titulo: 'Concierto de Otono', sala: 'Teatro Almendra' },
{ id: 'evt-002', titulo: 'Noche de Monologos', sala: 'Sala Boveda' },
{ id: 'evt-003', titulo: 'Festival de Jazz de Primavera', sala: 'Auditorio Ribera' }
];
// El resultado NO se devuelve: se entrega al callback.
// El error NO se lanza: se entrega al callback como primer argumento.
function buscarEvento(id, callback) {
// setTimeout simula la latencia de un disco o de una base de datos.
setTimeout(() => {
const evento = catalogo.find((e) => e.id === id);
if (!evento) {
callback(new Error(`Evento no encontrado: ${id}`));
return;
}
callback(null, evento);
}, 50);
}
// Uso: el resultado llega "hacia dentro" de la funcion, no "hacia fuera".
buscarEvento('evt-002', (error, evento) => {
if (error) {
console.error(`Error: ${error.message}`);
return;
}
console.log(`Encontrado: ${evento.titulo}`);
});
console.log('Esta linea se imprime ANTES que el resultado');Compara las dos versiones con atención, porque el cambio de mentalidad es todo:
| Aspecto | Síncrono | Asíncrono con callback |
|---|---|---|
| Entrega del resultado | return evento |
callback(null, evento) |
| Entrega del error | throw new Error(...) |
callback(new Error(...)) |
| Flujo del código | De arriba abajo | Fragmentado: el "después" vive dentro del callback |
| Captura de errores | try/catch |
Comprobar el primer argumento |
| Mientras espera | El proceso está bloqueado | El proceso atiende otras cosas |
Esa última fila es la razón de ser de todo. Si buscarEvento tuviera que leer de disco, la versión síncrona congelaría el servidor de Escena Viva durante toda la lectura; la asíncrona deja el hilo libre para atender a otros compradores.
- El patrón canónico de Node: el error-first callback
Node no dejó la forma del callback a la imaginación de cada uno. Fijó una convención que sigue toda su biblioteca estándar y prácticamente todo el ecosistema:
El callback recibe el error como primer argumento y el resultado como segundo. Si no hubo error, el primer argumento es
null.
Se le llama error-first callback o Node-style callback. Así se ve en la API real:
const fs = require('node:fs');
fs.readFile('datos/eventos.json', 'utf8', (error, contenido) => {
if (error) {
console.error(`No se pudo leer el catalogo: ${error.message}`);
return;
}
const catalogo = JSON.parse(contenido);
console.log(`Cargados ${catalogo.length} eventos`);
});Las reglas completas de la convención:
- El callback es siempre el último parámetro de la función.
- El primer argumento del callback es siempre el error, o
nullsi todo fue bien. - El error es un objeto
Error, no una cadena. UnErrorllevamessage,stacky —en Node— a menudo uncode('ENOENT','EACCES') que permite decidir sin analizar el texto. - Si hay error, no hay resultado. No se llama con las dos cosas a la vez.
Y el patrón de uso, que debes escribir en piloto automático:
funcionAsincrona(argumentos, (error, resultado) => {
if (error) {
// 1. Tratar el error
// 2. RETORNAR. Este return es obligatorio.
return;
}
// 3. Aqui, y solo aqui, el resultado es fiable
});El
returntras tratar el error no es opcional. Sin él, la ejecución continúa hacia el código del caso feliz conresultadovaliendoundefined, y el fallo real queda enterrado bajo unTypeError: Cannot read properties of undefined. Es, sin exagerar, el error número uno de quien empieza con callbacks.
¿Por qué el error primero y no el resultado?
Porque obliga a verlo. El error ocupa la primera posición de la firma, así que aparece en cada callback que escribes aunque no quieras. Si estuviera al final, sería trivial declarar (resultado) => {...} y no enterarse jamás de que algo puede fallar.
Es una decisión de diseño deliberada: hacer el camino correcto el camino fácil.
- Escribir tus propias funciones asíncronas
Vamos a construir la capa de datos asíncrona de Escena Viva. Usaremos setTimeout para simular la latencia que en el Módulo 3 será real (disco) y en el Módulo 7 será de red (base de datos). Esto no es un truco didáctico gratuito: es exactamente lo que hace un doble de prueba en el Módulo 9.
Crea src/laboratorio/datos-asincronos.js:
// src/laboratorio/datos-asincronos.js
// Capa de acceso a datos de Escena Viva con callbacks al estilo de Node.
// La latencia se simula con setTimeout; en el modulo 3 sera E/S real.
const catalogo = [
{
id: 'evt-001',
titulo: 'Concierto de Otono',
sala: 'Teatro Almendra',
organizador: 'org-almendra',
sesiones: [
{ id: 'ses-001-1', fechaHora: '2026-10-03T20:00:00', aforo: 420, vendidas: 180, precioCentimos: 2500 },
{ id: 'ses-001-2', fechaHora: '2026-10-04T19:00:00', aforo: 420, vendidas: 96, precioCentimos: 2200 }
]
},
{
id: 'evt-002',
titulo: 'Noche de Monologos',
sala: 'Sala Boveda',
organizador: 'org-boveda',
sesiones: [
{ id: 'ses-002-1', fechaHora: '2026-10-10T21:30:00', aforo: 120, vendidas: 118, precioCentimos: 1800 },
{ id: 'ses-002-2', fechaHora: '2026-10-11T21:30:00', aforo: 120, vendidas: 45, precioCentimos: 1800 },
{ id: 'ses-002-3', fechaHora: '2026-10-17T21:30:00', aforo: 120, vendidas: 12, precioCentimos: 1500 }
]
},
{
id: 'evt-003',
titulo: 'Festival de Jazz de Primavera',
sala: 'Auditorio Ribera',
organizador: 'org-ribera',
sesiones: [
{ id: 'ses-003-1', fechaHora: '2027-04-17T19:00:00', aforo: 900, vendidas: 640, precioCentimos: 3800 },
{ id: 'ses-003-2', fechaHora: '2027-04-18T19:00:00', aforo: 900, vendidas: 720, precioCentimos: 4200 }
]
}
];
// Latencia simulada, en milisegundos.
const LATENCIA_MS = 40;
// --- Busqueda de un evento por su id ---
function buscarEvento(id, callback) {
setTimeout(() => {
const evento = catalogo.find((e) => e.id === id);
if (!evento) {
// Error con codigo, igual que hace Node: permite decidir sin leer el texto.
const error = new Error(`Evento no encontrado: ${id}`);
error.codigo = 'EVENTO_NO_ENCONTRADO';
callback(error);
return;
}
callback(null, evento);
}, LATENCIA_MS);
}
// --- Busqueda de una sesion por su id, en todo el catalogo ---
function buscarSesion(sesionId, callback) {
setTimeout(() => {
for (const evento of catalogo) {
const sesion = evento.sesiones.find((s) => s.id === sesionId);
if (sesion) {
// Devolvemos tambien el evento: quien llama casi siempre lo necesita.
callback(null, { evento, sesion });
return;
}
}
const error = new Error(`Sesion no encontrada: ${sesionId}`);
error.codigo = 'SESION_NO_ENCONTRADA';
callback(error);
}, LATENCIA_MS);
}
// --- Reserva de entradas ---
function reservarEntradas(sesionId, cantidad, callback) {
// Validacion de argumentos ANTES de cualquier trabajo.
if (!Number.isInteger(cantidad) || cantidad < 1) {
const error = new Error('La cantidad debe ser un entero positivo');
error.codigo = 'CANTIDAD_INVALIDA';
// Ojo: aqui hay una trampa. La resolvemos en el apartado 5.
callback(error);
return;
}
buscarSesion(sesionId, (error, resultado) => {
if (error) {
callback(error);
return;
}
const { evento, sesion } = resultado;
const libres = sesion.aforo - sesion.vendidas;
if (cantidad > libres) {
const fallo = new Error(
`Aforo insuficiente en ${sesionId}: pides ${cantidad} y quedan ${libres}`
);
fallo.codigo = 'AFORO_INSUFICIENTE';
callback(fallo);
return;
}
// Como el JavaScript del usuario es monohilo, esta lectura-modificacion-escritura
// es atomica: nadie puede colarse entre las dos lineas.
sesion.vendidas += cantidad;
callback(null, {
eventoId: evento.id,
sesionId: sesion.id,
cantidad,
importeCentimos: cantidad * sesion.precioCentimos,
libresRestantes: sesion.aforo - sesion.vendidas
});
}, LATENCIA_MS);
}Pruébalo:
// Caso feliz
reservarEntradas('ses-002-2', 3, (error, reserva) => {
if (error) {
console.error(`No se pudo reservar: ${error.message}`);
return;
}
console.log(
`Reservadas ${reserva.cantidad} entradas de ${reserva.sesionId} ` +
`por ${(reserva.importeCentimos / 100).toFixed(2)} EUR. ` +
`Quedan ${reserva.libresRestantes}.`
);
});
// Caso de aforo insuficiente: ses-002-1 tiene 118 de 120 vendidas
reservarEntradas('ses-002-1', 5, (error) => {
if (error) {
console.error(`[${error.codigo}] ${error.message}`);
}
});Reservadas 3 entradas de ses-002-2 por 54.00 EUR. Quedan 72. [AFORO_INSUFICIENTE] Aforo insuficiente en ses-002-1: pides 5 y quedan 2
Fíjate en dos detalles de diseño que se repetirán en todo el curso:
error.codigoademás deerror.message. El mensaje es para las personas; el código es para el programa. En el Módulo 6 ese código se traducirá a un estado HTTP:SESION_NO_ENCONTRADA→ 404,AFORO_INSUFICIENTE→ 409.buscarSesiondevuelve{ evento, sesion }. Devolver la información que quien llama va a necesitar de todos modos evita una segunda consulta.
- La regla de oro: exactamente una vez, y siempre asíncronamente
Escribir funciones que reciben callbacks es fácil. Escribir funciones que aceptan callbacks correctamente tiene dos reglas que no admiten excepción.
Regla 1: llama al callback exactamente una vez
Ni cero veces (quien te llamó espera para siempre) ni dos (el código de después se ejecuta duplicado). Esta es la causa del error clásico que ya mencionamos:
// MAL: sin return, el callback se llama DOS veces cuando hay error.
function buscarEventoMal(id, callback) {
setTimeout(() => {
const evento = catalogo.find((e) => e.id === id);
if (!evento) {
callback(new Error('No encontrado')); // Falta el return
}
callback(null, evento); // Se ejecuta igualmente
}, 40);
}
buscarEventoMal('evt-999', (error, evento) => {
if (error) {
console.error('Error tratado');
return;
}
console.log(evento.titulo); // TypeError: Cannot read properties of undefined
});El error se trató correctamente… y aun así el proceso falló, porque el callback se invocó una segunda vez con undefined. Un return tras cada llamada al callback. Sin excepciones.
Regla 2: llama al callback siempre de forma asíncrona
Esta es más sutil y mucho más traicionera. Mira otra vez la validación de reservarEntradas:
function reservarEntradas(sesionId, cantidad, callback) {
if (!Number.isInteger(cantidad) || cantidad < 1) {
callback(error); // <-- Se llama SINCRONAMENTE
return;
}
buscarSesion(sesionId, (...) => {
callback(null, resultado); // <-- Se llama ASINCRONAMENTE
});
}Tenemos una función a veces síncrona y a veces asíncrona. Y eso rompe cosas:
// src/laboratorio/zalgo.js
// Demuestra el peligro de una funcion "a veces sincrona".
let estado = 'sin inicializar';
reservarEntradas('ses-002-2', -1, (error) => {
console.log(` Dentro del callback, estado = "${estado}"`);
});
estado = 'inicializado';
console.log(`Despues de la llamada, estado = "${estado}"`);El callback se ejecutó antes de que la línea siguiente hubiera corrido. Con una cantidad válida, en cambio, el orden habría sido el contrario. La misma función produce dos órdenes de ejecución distintos según sus argumentos.
Este problema tiene nombre propio en la comunidad de Node —"no liberes a Zalgo", por un artículo clásico de Isaac Schlueter— y produce errores de los peores: intermitentes, dependientes de los datos, imposibles de reproducir.
El remedio es process.nextTick, y este es exactamente uno de los dos casos legítimos que anunciamos en la lección del bucle de eventos:
// BIEN: el callback SIEMPRE se invoca de forma asincrona.
function reservarEntradas(sesionId, cantidad, callback) {
if (!Number.isInteger(cantidad) || cantidad < 1) {
const error = new Error('La cantidad debe ser un entero positivo');
error.codigo = 'CANTIDAD_INVALIDA';
// Aplazamos la llamada para garantizar asincronia uniforme.
process.nextTick(() => callback(error));
return;
}
// ... resto igual
}Ahora la salida es siempre la misma, con cualquier argumento:
| Situación | Qué usar |
|---|---|
| Ruta de error inmediata (validación de argumentos) | process.nextTick(() => callback(error)) |
| Resultado que ya tienes en memoria o en caché | process.nextTick(() => callback(null, valor)) |
| Trabajo largo que hay que trocear | setImmediate |
Nota profesional. La biblioteca estándar de Node cumple esta regla escrupulosamente.
fs.readFilecon una ruta inexistente no te llama de vuelta de forma síncrona: aplaza el error. Cuando escribas una API asíncrona propia, cúmplela tú también. Es la diferencia entre una biblioteca en la que se puede confiar y una que produce sustos.
- Por qué
try/catch no captura los errores asíncronos
try/catch no captura los errores asíncronosEste es el motivo profundo de que exista la convención error-first. Vamos a demostrarlo.
// src/laboratorio/try-catch-roto.js
// El try/catch NO captura lo que se lanza dentro de un callback asincrono.
function operacionQueFalla(callback) {
setTimeout(() => {
throw new Error('Fallo dentro del callback asincrono');
}, 50);
}
try {
operacionQueFalla();
console.log('El try ha terminado sin capturar nada');
} catch (error) {
console.error('Esto NUNCA se imprime:', error.message);
}
console.log('El script continua...');El try ha terminado sin capturar nada
El script continua...
/ruta/src/laboratorio/try-catch-roto.js:6
throw new Error('Fallo dentro del callback asincrono');
^
Error: Fallo dentro del callback asincrono
...
[el proceso muere con codigo 1]¿Por qué? Porque el bloque try ya había terminado cuando el error se lanzó. Recuerda el bucle de eventos:
sequenceDiagram
participant P as Pila de llamadas
participant B as Bucle de eventos
P->>P: entra en el bloque try
P->>B: setTimeout programa el callback
P->>P: sale del bloque try (¡ya terminó!)
P->>P: la pila se vacía
Note over B: pasan 50 ms
B->>P: ejecuta el callback (pila NUEVA)
P--xP: throw sin ningún try alrededor
Note over P: excepción no capturada → el proceso muere
try/catch protege una región de la pila de llamadas, y el callback se ejecuta en una pila completamente nueva, creada por el bucle de eventos mucho después. No hay ninguna relación entre ambas.
La consecuencia práctica, que hay que interiorizar:
Dentro de una función asíncrona, nunca lances un error hacia fuera. Pásalo al callback.
// MAL: nadie puede capturar esto.
function reservarMal(sesionId, cantidad, callback) {
setTimeout(() => {
if (cantidad > 10) throw new Error('Maximo 10 entradas por pedido');
callback(null, { sesionId, cantidad });
}, 40);
}
// BIEN: el error viaja por el canal previsto.
function reservarBien(sesionId, cantidad, callback) {
setTimeout(() => {
if (cantidad > 10) {
callback(new Error('Maximo 10 entradas por pedido'));
return;
}
callback(null, { sesionId, cantidad });
}, 40);
}Y una variante importante: sí puedes usar try/catch dentro del callback, porque ahí sí estás en la misma pila.
fs.readFile('datos/eventos.json', 'utf8', (error, contenido) => {
if (error) {
console.error(`Error de lectura: ${error.message}`);
return;
}
// JSON.parse es SINCRONO: aqui try/catch si funciona.
let catalogo;
try {
catalogo = JSON.parse(contenido);
} catch (fallo) {
console.error(`El catalogo no es JSON valido: ${fallo.message}`);
return;
}
console.log(`Cargados ${catalogo.length} eventos`);
});Como red de seguridad de último recurso existe process.on('uncaughtException'), pero no es un mecanismo de manejo de errores: cuando salta, el estado de tu aplicación es desconocido. Solo sirve para registrar el fallo y cerrar de forma ordenada. Lo veremos en el Módulo 11.
- La pirámide de la muerte en Escena Viva
Ahora el problema real. El flujo de compra completo de Escena Viva tiene cuatro pasos encadenados, y cada uno depende del anterior:
flowchart LR
A["1. Buscar el evento"] --> B["2. Comprobar el aforo<br/>de la sesion"]
B --> C["3. Crear el pedido<br/>estado: pendiente"]
C --> D["4. Emitir las entradas<br/>y pasar a emitido"]
Con callbacks, "depende del anterior" significa "va dentro del anterior". Y el resultado es esto:
// src/laboratorio/compra-piramide.js
// El flujo de compra completo. Funciona, y es una pesadilla de mantener.
comprarEntradas('asis-001', 'evt-002', 'ses-002-2', 3);
function comprarEntradas(usuarioId, eventoId, sesionId, cantidad) {
buscarEvento(eventoId, (error, evento) => {
if (error) {
console.error(`[compra] no se encontro el evento: ${error.message}`);
return;
}
comprobarAforo(sesionId, cantidad, (error, sesion) => {
if (error) {
console.error(`[compra] aforo: ${error.message}`);
return;
}
crearPedido(usuarioId, sesion, cantidad, (error, pedido) => {
if (error) {
console.error(`[compra] no se pudo crear el pedido: ${error.message}`);
return;
}
cobrarPedido(pedido, (error, pedidoPagado) => {
if (error) {
// Y ademas hay que DESHACER: liberar el aforo reservado.
liberarAforo(sesionId, cantidad, (errorLiberar) => {
if (errorLiberar) {
console.error(`[compra] fallo al liberar aforo: ${errorLiberar.message}`);
}
console.error(`[compra] cobro rechazado: ${error.message}`);
});
return;
}
emitirEntradas(pedidoPagado, (error, entradas) => {
if (error) {
console.error(`[compra] no se pudieron emitir las entradas: ${error.message}`);
return;
}
console.log(`Pedido ${pedidoPagado.id} completado`);
console.log(`Evento : ${evento.titulo}`);
console.log(`Importe: ${(pedidoPagado.totalCentimos / 100).toFixed(2)} EUR`);
for (const entrada of entradas) {
console.log(` ${entrada.codigo} ${entrada.estado}`);
}
});
});
});
});
});
}A esto se le llama pirámide de la muerte (pyramid of doom) o infierno de callbacks (callback hell), y no es una cuestión estética. Los problemas son concretos y medibles:
| Problema | Por qué duele |
|---|---|
| Indentación creciente | Con seis niveles, el código útil empieza en la columna 30. En pantallas normales no cabe |
| Manejo de errores repetido | El mismo if (error) { ...; return; } cinco veces, y cada uno hay que escribirlo a mano |
| Deshacer es un infierno | Mira el liberarAforo anidado dentro del error del cobro: si hubiera tres cosas que deshacer, serían tres niveles más |
| Imposible de leer en orden | Para saber qué pasa después del paso 3 hay que bajar, no seguir leyendo |
| Difícil de reutilizar | Ninguno de esos pasos se puede extraer sin reescribirlo todo |
| Imposible paralelizar | Si los pasos 1 y 2 fueran independientes, con esta estructura seguirían ejecutándose en serie |
| Variables atrapadas en el cierre | evento solo está disponible dentro de su nivel; para usarlo abajo hay que arrastrarlo por toda la pirámide |
Y falta lo peor: este ejemplo es corto. Un flujo real añade validación de usuario, comprobación de descuentos, registro de auditoría y envío de correo de confirmación. Diez niveles no es una exageración.
- Técnicas clásicas de mitigación
Antes de las promesas, la comunidad desarrolló técnicas para hacer esto habitable. Siguen siendo válidas y buenas prácticas de diseño en sí mismas.
8.1 Funciones con nombre en lugar de anónimas
En vez de anidar funciones anónimas, se declara cada paso por separado y se pasa por referencia:
// src/laboratorio/compra-plana.js
// Mismo flujo, aplanado con funciones con nombre.
function comprarEntradas(usuarioId, eventoId, sesionId, cantidad) {
// Contexto compartido por todos los pasos: sustituye a los cierres anidados.
const contexto = { usuarioId, eventoId, sesionId, cantidad };
buscarEvento(eventoId, alEncontrarEvento);
function alEncontrarEvento(error, evento) {
if (error) return fallo('evento', error);
contexto.evento = evento;
comprobarAforo(sesionId, cantidad, alComprobarAforo);
}
function alComprobarAforo(error, sesion) {
if (error) return fallo('aforo', error);
contexto.sesion = sesion;
crearPedido(usuarioId, sesion, cantidad, alCrearPedido);
}
function alCrearPedido(error, pedido) {
if (error) return fallo('pedido', error);
contexto.pedido = pedido;
cobrarPedido(pedido, alCobrar);
}
function alCobrar(error, pedidoPagado) {
if (error) return compensarYFallar(error);
contexto.pedido = pedidoPagado;
emitirEntradas(pedidoPagado, alEmitir);
}
function alEmitir(error, entradas) {
if (error) return fallo('emision', error);
mostrarResumen(contexto, entradas);
}
// Un unico punto de tratamiento de errores para todo el flujo.
function fallo(paso, error) {
console.error(`[compra] fallo en el paso "${paso}": ${error.message}`);
}
function compensarYFallar(error) {
liberarAforo(sesionId, cantidad, (errorLiberar) => {
if (errorLiberar) {
console.error(`[compra] CRITICO: aforo no liberado: ${errorLiberar.message}`);
}
fallo('cobro', error);
});
}
}Ganancias inmediatas:
- Indentación constante. Ningún nivel supera dos.
- El flujo se lee de arriba abajo, en el mismo orden en que ocurre.
- Un único punto de tratamiento de errores (
fallo), no cinco copias. - Cada paso es una función con nombre, lo que significa que aparece con su nombre en las trazas de error y se puede probar por separado (Módulo 9).
El precio es el objeto contexto: al perder los cierres anidados hay que llevar el estado a mano. Es un intercambio razonable.
8.2 Retorno temprano
Ya lo hemos usado, pero merece enunciarse como técnica: trata el error y sal, en lugar de envolver el caso feliz en un else.
// Peor: el caso feliz queda indentado y el else se acumula.
buscarEvento(id, (error, evento) => {
if (error) {
console.error(error.message);
} else {
console.log(evento.titulo);
}
});
// Mejor: el caso feliz queda al nivel principal.
buscarEvento(id, (error, evento) => {
if (error) return console.error(error.message);
console.log(evento.titulo);
});8.3 Modularizar
Si una función tiene más de dos niveles de callback, casi siempre está haciendo más de una cosa. En el flujo de compra, crearPedido + cobrarPedido + compensar es una unidad conceptual (procesar el pago) que puede vivir en su propio fichero y exponer un solo callback.
En la lección Módulos CommonJS y require() veremos cómo hacer esa separación de verdad. La idea de fondo: la profundidad de la pirámide suele ser un síntoma de mala separación de responsabilidades, no solo de la sintaxis de los callbacks.
8.4 Un ayudante para ejecutar pasos en serie
Cuando los pasos tienen la misma forma, se puede escribir un pequeño motor:
// src/utiles/en-serie.js
// Ejecuta una lista de pasos en orden, pasando el contexto de uno a otro.
// Cada paso tiene la firma: (contexto, siguiente) => void
// donde siguiente es un error-first callback.
function enSerie(pasos, contexto, alTerminar) {
let indice = 0;
function siguiente(error) {
if (error) {
alTerminar(error, contexto);
return;
}
if (indice >= pasos.length) {
alTerminar(null, contexto);
return;
}
const paso = pasos[indice++];
// Aplazamos para garantizar asincronia uniforme (regla de oro 2).
process.nextTick(() => paso(contexto, siguiente));
}
siguiente();
}
module.exports = { enSerie };Uso:
enSerie(
[
(ctx, siguiente) => buscarEvento(ctx.eventoId, (e, evento) => {
ctx.evento = evento;
siguiente(e);
}),
(ctx, siguiente) => comprobarAforo(ctx.sesionId, ctx.cantidad, (e, sesion) => {
ctx.sesion = sesion;
siguiente(e);
}),
(ctx, siguiente) => crearPedido(ctx.usuarioId, ctx.sesion, ctx.cantidad, (e, pedido) => {
ctx.pedido = pedido;
siguiente(e);
})
],
{ usuarioId: 'asis-001', eventoId: 'evt-002', sesionId: 'ses-002-2', cantidad: 3 },
(error, contexto) => {
if (error) return console.error(`[compra] ${error.message}`);
console.log(`Pedido ${contexto.pedido.id} creado para ${contexto.evento.titulo}`);
}
);Esto es, en esencia, lo que hacía la biblioteca async, omnipresente en el ecosistema Node entre 2011 y 2016. Si te encuentras async.series, async.waterfall o async.parallel en código heredado, ya sabes qué son.
Y si te parece que sigue siendo mucha maquinaria para algo que debería ser sencillo… tienes toda la razón. Esa es exactamente la conclusión a la que llegó la comunidad, y por eso llegaron las promesas.
- Ventajas e inconvenientes de los callbacks
| Ventajas | Inconvenientes |
|---|---|
| Simplicidad conceptual: es solo una función que se pasa como argumento | Anidamiento: los flujos secuenciales crecen en profundidad |
| Sin coste de rendimiento: no hay objeto intermedio ni cola de microtareas | Manejo de errores repetitivo: if (error) return en cada nivel |
| Universales: los soporta cualquier versión de Node y del navegador | try/catch no funciona: el modelo de errores del lenguaje no aplica |
| Perfectos para eventos repetidos: un callback puede llamarse muchas veces | Fáciles de usar mal: llamar dos veces, no llamar, llamar síncronamente |
Necesarios para streams y EventEmitter |
Componer es difícil: encadenar, paralelizar o cancelar requiere ayudantes |
| Menor consumo de memoria en operaciones muy frecuentes | Inversión de control: entregas tu función a un tercero y confías en que la llame bien |
Ese último inconveniente merece una nota. Cuando pasas un callback a una biblioteca, estás confiando en que la llame una vez, con los argumentos correctos y de forma asíncrona. Si la biblioteca tiene un fallo, tú sufres las consecuencias y depurarlo es dificilísimo. Las promesas eliminan este problema de raíz, porque el contrato lo impone el lenguaje y no cada autor.
- Por qué los callbacks siguen importando
Con async/await disponible desde Node 7.6, ¿por qué dedicar una lección entera a los callbacks? Cuatro razones muy prácticas:
1. Hay APIs de Node que solo aceptan callbacks.
No todo tiene versión con promesas. fs.watch, dns.lookup, gran parte de crypto, muchas opciones de child_process y —sobre todo— todo lo basado en eventos siguen siendo territorio de callbacks.
2. EventEmitter es puramente de callbacks.
Cuando escribas emisor.on('sesion-agotada', (datos) => {...}), eso es un callback. Y no puede ser una promesa, porque una promesa se resuelve una sola vez y un evento se emite muchas. Es el tema de la lección Eventos y EventEmitter.
3. Los streams funcionan con callbacks y eventos.
Todo el Módulo 3 se apoya en stream.on('data', ...), stream.on('end', ...) y en callbacks de finalización.
4. El middleware de Express es un callback.
La firma (req, res, next) del Módulo 6 es exactamente el patrón que acabas de aprender, con next haciendo el papel de "continúa con el siguiente paso".
Dicho de otro modo: las promesas sustituyen a los callbacks para la asincronía de un solo resultado, no para todo lo demás. Saber cuándo usar cada uno es parte de escribir Node profesionalmente, y a eso dedicaremos una tabla de decisión en la lección de EventEmitter.
Errores Comunes y Consejos
Error 1: olvidar el return después de tratar el error.
El callback se llama dos veces y el fallo real queda enterrado bajo un TypeError. Es el error número uno.
Error 2: lanzar excepciones dentro de un callback asíncrono. Nadie las captura; el proceso muere. Pásalas siempre por el primer argumento del callback.
Error 3: envolver una llamada asíncrona en try/catch y creer que estás protegido.
El try ya ha terminado cuando el callback se ejecuta.
Error 4: escribir funciones "a veces síncronas".
El orden de ejecución cambia según los datos y aparecen errores irreproducibles. process.nextTick en la ruta rápida.
Error 5: intentar devolver un valor desde un callback.
// Esto NO funciona: buscarEvento devuelve undefined mucho antes.
function obtenerTitulo(id) {
let titulo;
buscarEvento(id, (error, evento) => { titulo = evento.titulo; });
return titulo; // undefined, siempre
}No hay forma de convertir asíncrono en síncrono. La única salida es propagar la asincronía hacia arriba.
Error 6: usar callbacks dentro de forEach esperando que se respete el orden.
forEach no espera a nada. Los callbacks terminan en orden aleatorio y no hay forma de saber cuándo acabaron todos.
Consejo 1: escribe siempre (error, resultado), no (err, res). En un manejador de Express, res significa otra cosa muy distinta, y la confusión es real.
Consejo 2: pon un codigo a tus errores. El mensaje es para las personas; el código es lo que tu código usa para decidir.
Consejo 3: si llevas tres niveles de anidamiento, para y extrae funciones. No sigas escribiendo hacia la derecha.
Consejo 4: no reescribas código de callbacks que funciona solo por moda. Aprende a convertirlo cuando toque —con util.promisify, en la lección siguiente— pero un fs.watch con su callback no necesita ninguna mejora.
Ejercicios
Ejercicio 1: arreglar una función asíncrona rota
Esta función tiene cuatro defectos según lo aprendido en la lección. Encuéntralos todos, explica qué consecuencia tiene cada uno y reescríbela correctamente.
function obtenerOcupacion(sesionId, callback) {
if (!sesionId) {
callback('Falta el identificador de sesion');
}
setTimeout(() => {
const sesion = buscarSesionEnMemoria(sesionId);
if (!sesion) {
callback(new Error('No encontrada'));
}
try {
const porcentaje = Math.round((sesion.vendidas / sesion.aforo) * 100);
callback(null, porcentaje);
} catch (error) {
throw error;
}
}, 30);
}Ejercicio 2: el informe de sesiones en riesgo, en asíncrono
En la lección Tu Primer Programa en Node.js escribiste un informe síncrono de sesiones con menos del 20 % vendido. Reescríbelo con la capa asíncrona de esta lección.
Escribe src/laboratorio/informe-riesgo.js con:
- Una función
listarSesionesEnRiesgo(umbralPorcentaje, callback)que usebuscarEventopara cargar los tres eventos uno a uno, en serie (los ids sonevt-001,evt-002yevt-003) y devuelva por callback un array de objetos{ eventoId, titulo, sesionId, fechaHora, porcentaje }. - Manejo de errores correcto: si falla cualquier evento, el callback debe recibir el error y no debe llamarse más veces.
- Salida por
stdoutconconsole.tableyprocess.exitCode = 1si hay alguna sesión en riesgo. - El umbral debe poder pasarse por línea de comandos:
node src/laboratorio/informe-riesgo.js 25.
Al terminar, responde: ¿cuánto tarda tu solución con una latencia de 40 ms por evento? ¿Cuánto tardaría si los tres eventos se cargaran a la vez? ¿Por qué con esta estructura no puedes hacerlo?
Ejercicio 3: aplanar la pirámide de compra
Toma el flujo de comprarEntradas del apartado 7 e impleméntalo de forma completa y ejecutable, con estas piezas simuladas (todas con latencia de 30 ms y firma error-first):
crearPedido(usuarioId, sesion, cantidad, callback)→ devuelve{ id: 'ped-001', usuarioId, sesionId, cantidad, totalCentimos, estado: 'pendiente' }.cobrarPedido(pedido, callback)→ falla con códigoPAGO_RECHAZADOsitotalCentimos > 20000; si no, devuelve el pedido conestado: 'pagado'.emitirEntradas(pedido, callback)→ devuelve un array decantidadentradas{ codigo: 'EV-2026-000001', sesionId, estado: 'valida' }y deja el pedido enestado: 'emitido'.liberarAforo(sesionId, cantidad, callback)→ devuelve el número de libres tras liberar.
Requisitos:
- Estructura plana, con funciones con nombre y un objeto
contexto(técnica 8.1). - Un único punto de tratamiento de errores.
- Compensación correcta: si el cobro falla, se libera el aforo antes de informar.
- Prueba los dos caminos:
ses-002-2con 3 entradas (54,00 EUR, debe funcionar) yses-003-2con 5 entradas (210,00 EUR, debe rechazarse y liberar el aforo).
Soluciones
Solución 1
Los cuatro defectos:
| # | Defecto | Consecuencia |
|---|---|---|
| 1 | Falta el return tras callback('Falta el identificador...') |
La ejecución continúa al setTimeout y el callback se llama dos veces |
| 2 | El error es una cadena, no un objeto Error |
Quien lo recibe no tiene stack ni code; además rompe la convención |
| 3 | Llamada síncrona en la ruta de validación | Función "a veces síncrona": Zalgo. El orden de ejecución cambia según los argumentos |
| 4 | Falta el return tras callback(new Error('No encontrada')) |
Se ejecuta el try con sesion valiendo undefined: TypeError |
Y un quinto defecto de bonus: el try/catch que hace throw error es inútil y dañino. Captura la excepción para volver a lanzarla dentro de un callback asíncrono, donde nadie puede recogerla y donde tumbará el proceso.
Versión corregida:
// src/laboratorio/obtener-ocupacion.js
// Devuelve el porcentaje de ocupacion de una sesion.
function obtenerOcupacion(sesionId, callback) {
// 1. Validacion de argumentos, aplazada para garantizar asincronia uniforme.
if (!sesionId) {
const error = new Error('Falta el identificador de sesion');
error.codigo = 'ARGUMENTO_INVALIDO';
process.nextTick(() => callback(error));
return;
}
setTimeout(() => {
const sesion = buscarSesionEnMemoria(sesionId);
// 2. Error con return: el callback se llama exactamente una vez.
if (!sesion) {
const error = new Error(`Sesion no encontrada: ${sesionId}`);
error.codigo = 'SESION_NO_ENCONTRADA';
callback(error);
return;
}
// 3. Proteccion real ante datos incoherentes, en lugar de un try/catch inutil.
if (!sesion.aforo || sesion.aforo <= 0) {
const error = new Error(`Aforo invalido en ${sesionId}: ${sesion.aforo}`);
error.codigo = 'DATO_INCOHERENTE';
callback(error);
return;
}
const porcentaje = Math.round((sesion.vendidas / sesion.aforo) * 100);
callback(null, porcentaje);
}, 30);
}Solución 2
// src/laboratorio/informe-riesgo.js
// Sesiones con menos del umbral de ocupacion, cargando los eventos en serie.
const EVENTOS = ['evt-001', 'evt-002', 'evt-003'];
const UMBRAL_POR_DEFECTO = 20;
function listarSesionesEnRiesgo(umbralPorcentaje, callback) {
const enRiesgo = [];
let indice = 0;
let terminado = false; // Guarda contra llamadas multiples al callback.
function siguienteEvento() {
if (indice >= EVENTOS.length) {
finalizar(null, enRiesgo);
return;
}
const eventoId = EVENTOS[indice++];
buscarEvento(eventoId, (error, evento) => {
if (error) {
finalizar(error);
return;
}
for (const sesion of evento.sesiones) {
const porcentaje = Math.round((sesion.vendidas / sesion.aforo) * 100);
if (porcentaje < umbralPorcentaje) {
enRiesgo.push({
eventoId: evento.id,
titulo: evento.titulo,
sesionId: sesion.id,
fechaHora: sesion.fechaHora,
porcentaje
});
}
}
siguienteEvento();
});
}
// Garantiza que el callback se invoque exactamente una vez.
function finalizar(error, resultado) {
if (terminado) return;
terminado = true;
callback(error, resultado);
}
siguienteEvento();
}
// --- Punto de entrada ---
const umbral = Number(process.argv[2]) || UMBRAL_POR_DEFECTO;
const inicio = Date.now();
console.error(`Buscando sesiones con menos del ${umbral}% vendido...`);
listarSesionesEnRiesgo(umbral, (error, sesiones) => {
if (error) {
console.error(`No se pudo generar el informe: ${error.message}`);
process.exitCode = 2;
return;
}
console.error(`Consultados ${EVENTOS.length} eventos en ${Date.now() - inicio} ms`);
if (sesiones.length === 0) {
console.log('Sin sesiones en riesgo.');
return;
}
console.table(sesiones);
process.exitCode = 1; // Codigo distinto de 0 para un sistema de avisos.
});Buscando sesiones con menos del 20% vendido... Consultados 3 eventos en 128 ms ┌─────────┬───────────┬──────────────────────┬─────────────┬───────────────────────┬────────────┐ │ (index) │ eventoId │ titulo │ sesionId │ fechaHora │ porcentaje │ ├─────────┼───────────┼──────────────────────┼─────────────┼───────────────────────┼────────────┤ │ 0 │ 'evt-002' │ 'Noche de Monologos' │ 'ses-002-3' │ '2026-10-17T21:30:00' │ 10 │ └─────────┴───────────┴──────────────────────┴─────────────┴───────────────────────┴────────────┘
Respuestas a las preguntas:
- Con 40 ms por evento en serie: ~120 ms. Los tiempos se suman porque cada consulta empieza cuando termina la anterior.
- Si se cargaran a la vez: ~40 ms. El coste sería el del evento más lento, no la suma.
- Por qué no puedes hacerlo con esta estructura:
siguienteEventoestá construida sobre la premisa de que cada paso llama al siguiente. Para paralelizar harían falta un contador de respuestas pendientes, un array de resultados indexado y una guarda para llamar al callback final solo cuando el contador llegue a cero — y todo ello sin llamar dos veces al callback si uno falla. Es perfectamente posible (es lo que hacíaasync.parallel), pero es maquinaria manual y propensa a errores. En la lección siguiente,Promise.allresuelve exactamente esto en una línea.
Solución 3
// src/laboratorio/compra-plana.js
// Flujo de compra completo de Escena Viva, con estructura plana y compensacion.
const LATENCIA_MS = 30;
let secuenciaPedido = 0;
let secuenciaEntrada = 0;
// --- Pasos simulados ---
function crearPedido(usuarioId, sesion, cantidad, callback) {
setTimeout(() => {
secuenciaPedido++;
callback(null, {
id: `ped-${String(secuenciaPedido).padStart(3, '0')}`,
usuarioId,
sesionId: sesion.id,
cantidad,
totalCentimos: cantidad * sesion.precioCentimos,
estado: 'pendiente'
});
}, LATENCIA_MS);
}
function cobrarPedido(pedido, callback) {
setTimeout(() => {
if (pedido.totalCentimos > 20000) {
const error = new Error(
`Pago rechazado: ${(pedido.totalCentimos / 100).toFixed(2)} EUR supera el limite`
);
error.codigo = 'PAGO_RECHAZADO';
callback(error);
return;
}
callback(null, { ...pedido, estado: 'pagado' });
}, LATENCIA_MS);
}
function emitirEntradas(pedido, callback) {
setTimeout(() => {
const anio = new Date().getFullYear();
const entradas = [];
for (let i = 0; i < pedido.cantidad; i++) {
secuenciaEntrada++;
entradas.push({
codigo: `EV-${anio}-${String(secuenciaEntrada).padStart(6, '0')}`,
sesionId: pedido.sesionId,
pedidoId: pedido.id,
estado: 'valida'
});
}
pedido.estado = 'emitido';
callback(null, entradas);
}, LATENCIA_MS);
}
function liberarAforo(sesionId, cantidad, callback) {
setTimeout(() => {
buscarSesion(sesionId, (error, resultado) => {
if (error) {
callback(error);
return;
}
resultado.sesion.vendidas -= cantidad;
callback(null, resultado.sesion.aforo - resultado.sesion.vendidas);
});
}, LATENCIA_MS);
}
// --- Flujo de compra, plano ---
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) {
if (error) return fallo('reservar-aforo', error);
contexto.aforoReservado = true;
contexto.reserva = reserva;
buscarSesion(sesionId, alBuscarSesion);
}
function alBuscarSesion(error, resultado) {
if (error) return compensarYFallar('buscar-sesion', error);
contexto.sesion = resultado.sesion;
crearPedido(usuarioId, resultado.sesion, cantidad, alCrearPedido);
}
function alCrearPedido(error, pedido) {
if (error) return compensarYFallar('crear-pedido', error);
contexto.pedido = pedido;
cobrarPedido(pedido, alCobrar);
}
function alCobrar(error, pedidoPagado) {
if (error) return compensarYFallar('cobrar', error);
contexto.pedido = pedidoPagado;
emitirEntradas(pedidoPagado, alEmitir);
}
function alEmitir(error, entradas) {
if (error) return compensarYFallar('emitir', error);
contexto.entradas = entradas;
alTerminar(null, contexto);
}
// Punto unico de error, sin compensacion.
function fallo(paso, error) {
error.paso = paso;
alTerminar(error, contexto);
}
// Punto unico de error CON compensacion del aforo ya reservado.
function compensarYFallar(paso, error) {
if (!contexto.aforoReservado) return fallo(paso, error);
liberarAforo(sesionId, cantidad, (errorLiberar, libres) => {
if (errorLiberar) {
console.error(`CRITICO: aforo no liberado en ${sesionId}: ${errorLiberar.message}`);
} else {
console.error(`[compensacion] aforo liberado en ${sesionId}, quedan ${libres} libres`);
}
fallo(paso, error);
});
}
}
// --- Pruebas de los dos caminos ---
function mostrar(error, contexto) {
if (error) {
console.error(`[${error.codigo || 'ERROR'}] fallo en "${error.paso}": ${error.message}`);
return;
}
console.log('');
console.log(`Pedido ${contexto.pedido.id} - ${contexto.pedido.estado}`);
console.log(` Evento : ${contexto.evento.titulo}`);
console.log(` Sesion : ${contexto.sesion.id}`);
console.log(` Importe: ${(contexto.pedido.totalCentimos / 100).toFixed(2)} EUR`);
for (const entrada of contexto.entradas) {
console.log(` ${entrada.codigo} ${entrada.estado}`);
}
}
// Camino feliz: 3 x 18,00 = 54,00 EUR
comprarEntradas('asis-001', 'evt-002', 'ses-002-2', 3, mostrar);
// Camino de rechazo: 5 x 42,00 = 210,00 EUR, supera el limite
comprarEntradas('asis-002', 'evt-003', 'ses-003-2', 5, mostrar);Salida:
Pedido ped-001 - emitido Evento : Noche de Monologos Sesion : ses-002-2 Importe: 54.00 EUR EV-2026-000001 valida EV-2026-000002 valida EV-2026-000003 valida [compensacion] aforo liberado en ses-003-2, quedan 180 libres [PAGO_RECHAZADO] fallo en "cobrar": Pago rechazado: 210.00 EUR supera el limite
Lo importante de esta solución no es que funcione, sino cuánto trabajo estructural ha costado que funcione:
- Un objeto
contextopara arrastrar el estado, porque los cierres anidados desaparecieron. - Una bandera
aforoReservadopara saber si hay algo que compensar. - Dos puntos de salida de error (
falloycompensarYFallar) en lugar de uno. - Un callback anidado más dentro de la propia compensación.
Todo esto es contabilidad manual que el lenguaje no te ayuda a llevar. Un try/finally haría el trabajo de la compensación en tres líneas… si try/catch funcionara con código asíncrono. Y esa es, justamente, la primera cosa que las promesas devuelven.
Conclusión
Has aprendido el patrón que sostiene la asincronía clásica de Node. Un callback es una función que entregas para que se llame más tarde, y Node fijó para ella una forma canónica: el error-first callback (error, resultado), con el error siempre en primer lugar —para que sea imposible ignorarlo— y null cuando todo fue bien. Has escrito la capa de datos asíncrona de Escena Viva con esa firma: buscarEvento, buscarSesion y reservarEntradas, con errores que llevan un codigo propio (AFORO_INSUFICIENTE, SESION_NO_ENCONTRADA) que en el Módulo 6 se traducirá directamente a estados HTTP.
Has interiorizado las dos reglas que no admiten excepción: llamar al callback exactamente una vez —de ahí el return obligatorio tras cada llamada— y llamarlo siempre de forma asíncrona, incluso en la ruta de validación rápida, usando process.nextTick para no "liberar a Zalgo" con una función que unas veces es síncrona y otras no. Y has visto, demostrado paso a paso sobre la pila de llamadas, por qué try/catch no captura los errores lanzados dentro de un callback asíncrono: el bloque try ya terminó cuando el bucle de eventos ejecuta tu función en una pila nueva.
Después construiste la pirámide de la muerte con el flujo de compra real de Escena Viva —buscar evento, comprobar aforo, crear pedido, cobrar, emitir entradas— y comprobaste que el problema no es estético: es el manejo de errores duplicado cinco veces, la compensación anidada dentro del error del cobro, las variables atrapadas en cada cierre y la imposibilidad de paralelizar lo que es independiente. Las técnicas clásicas de mitigación —funciones con nombre, retorno temprano, modularizar y un ayudante enSerie— la hicieron habitable, pero al precio de llevar el estado a mano en un objeto contexto y de escribir maquinaria que el lenguaje debería aportar.
Y aun así, los callbacks no son historia: los necesitas para EventEmitter, para los streams del Módulo 3, para el middleware de Express y para las muchas APIs de Node que nunca tendrán versión con promesas.
Lo que falta es una forma de que la asincronía vuelva a parecerse al código normal: que un resultado se devuelva, que un error se lance y se capture con try/catch, que los pasos independientes se lancen a la vez con una sola instrucción y que la compensación se escriba en un finally. Eso es exactamente lo que ofrece la siguiente lección, Promesas y async/await, donde convertirás con util.promisify las funciones que acabas de escribir y reescribirás esta misma pirámide de compra hasta dejarla plana y legible de arriba abajo.
Curso de Node.js: De Principiante a Avanzado
Módulo 1: Introducción a Node.js
- ¿Qué es Node.js?
- Instalación y Configuración del Entorno
- Tu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Moderno para Node.js
- El Proyecto del Curso: la Plataforma Escena Viva
Módulo 2: Conceptos Básicos
- Arquitectura de Node.js
- El Bucle de Eventos (Event Loop)
- Callbacks y Programación Asíncrona
- Promesas y async/await
- Eventos y EventEmitter
- Módulos CommonJS y require()
- Módulos ES e Interoperabilidad
Módulo 3: Sistema de Archivos y E/S
- Lectura y Escritura de Archivos
- El Módulo fs a Fondo
- Rutas Multiplataforma con el Módulo path
- Trabajando con Streams
- Streams de Transformación y pipeline
- Buffers y Datos Binarios
Módulo 4: HTTP y Servidores Web
- Creando un Servidor HTTP Simple
- Manejo de Solicitudes y Respuestas
- Enrutamiento Manual
- Sirviendo Archivos Estáticos
- Recibiendo Datos: Cuerpos de Petición y JSON
- Consumiendo APIs Externas desde Node.js
Módulo 5: NPM y Gestión de Paquetes
- Introducción a NPM y package.json
- Instalación y Uso de Paquetes
- Versionado Semántico y package-lock
- Scripts de npm y Automatización del Proyecto
- Creación y Publicación de Paquetes
- Seguridad y Mantenimiento de Dependencias
Módulo 6: Framework Express.js
- Introducción a Express.js
- Configuración de una Aplicación Express
- Enrutamiento en Express
- Middleware
- Middleware de Terceros Esenciales
- Validación de Datos de Entrada
- Manejo de Errores
Módulo 7: Bases de Datos y ORMs
- Introducción a las Bases de Datos
- Usando MongoDB con Mongoose
- Operaciones CRUD
- Relaciones, Poblado y Consultas Avanzadas
- Usando Bases de Datos SQL con Sequelize
- Migraciones, Transacciones y Datos de Prueba
Módulo 8: Autenticación y Autorización
- Introducción a la Autenticación
- Registro de Usuarios y Hash de Contraseñas
- Sesiones y Cookies con Passport.js
- Autenticación con JWT
- Control de Acceso Basado en Roles
- Buenas Prácticas de Seguridad en APIs
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con Mocha y Chai
- Dobles de Prueba con Sinon
- Pruebas de Integración
- Cobertura y Automatización de las Pruebas
- Depuración de Aplicaciones Node.js
Módulo 10: Temas Avanzados
- El Módulo Cluster
- Hilos de Trabajo (Worker Threads)
- Caché y Colas de Trabajo con Redis
- Optimización del Rendimiento
- Construcción de APIs RESTful
- GraphQL con Node.js
Módulo 11: Despliegue y DevOps
- Configuración y Variables de Entorno
- Registro y Monitorización en Producción
- Usando PM2 para la Gestión de Procesos
- Empaquetado con Docker
- Desplegando en Heroku y Otras PaaS
- Integración y Despliegue Continuos
