CAP 06 · LEC 01·Asincronía

Callbacks: funciones que se ejecutan después

Un callback es una función que pasas como argumento para que otra función la ejecute más tarde. Es el mecanismo más primitivo de asincronía en JavaScript — y la base de todo lo demás.

● INTERMEDIO8 min lectura4 ejerciciospor Fernando Herrera · actualizado mayo de 2026
¿Encontraste un error o algo que mejorar?Editá esta lección en GitHub →

¿Qué es un callback?

Un callback no es más que una función que se pasa como argumento. La función receptora decide cuándo y cómo llamarla:

function procesar(valor, alTerminar) { const resultado = valor * 2; alTerminar(resultado); } procesar(5, (resultado) => { console.log(resultado); // 10 }); // También puedes pasar funciones con nombre function mostrarResultado(valor) { console.log(`Resultado: ${valor}`); } procesar(7, mostrarResultado); // Resultado: 14
function procesar(valor: number, alTerminar: (resultado: number) => void): void { const resultado = valor * 2; alTerminar(resultado); } procesar(5, (resultado) => { console.log(resultado); // 10 }); // También puedes pasar funciones con nombre function mostrarResultado(valor: number): void { console.log(`Resultado: ${valor}`); } procesar(7, mostrarResultado); // Resultado: 14
Salida10 Resultado: 14

Callbacks síncronos vs asíncronos

No todo callback es asíncrono. forEach, map y filter usan callbacks síncronos — se ejecutan de inmediato:

// Callback SÍNCRONO — se ejecuta de inmediato const numeros = [1, 2, 3]; numeros.forEach(n => { console.log(n); }); console.log("Fin del forEach"); // 1 // 2 // 3 // Fin del forEach // Callback ASÍNCRONO — se ejecuta más tarde console.log("Antes del timeout"); setTimeout(() => { console.log("Dentro del timeout"); }, 0); console.log("Después del timeout"); // Antes del timeout // Después del timeout // Dentro del timeout
// Callback SÍNCRONO — se ejecuta de inmediato const numeros: number[] = [1, 2, 3]; numeros.forEach((n: number) => { console.log(n); }); console.log("Fin del forEach"); // 1 // 2 // 3 // Fin del forEach // Callback ASÍNCRONO — se ejecuta más tarde console.log("Antes del timeout"); setTimeout(() => { console.log("Dentro del timeout"); }, 0); console.log("Después del timeout"); // Antes del timeout // Después del timeout // Dentro del timeout
Salida1 2 3 Fin del forEach Antes del timeout Después del timeout Dentro del timeout
¿Por qué setTimeout(fn, 0) no es inmediato?

Aunque el delay es 0ms, setTimeout siempre encola el callback en la macrotask queue. El código síncrono actual termina antes de que la queue se procese. El Event Loop explica esto en detalle en la lección 06-05.

El problema: Callback Hell

Cuando necesitas operaciones asíncronas en secuencia, los callbacks se anidan — creando la temida pirámide de doom:

// Simulación de operaciones asíncronas anidadas function obtenerUsuario(id, callback) { setTimeout(() => callback(null, { id, nombre: "Ana" }), 100); } function obtenerPedidos(usuarioId, callback) { setTimeout(() => callback(null, [{ id: 1, total: 50 }]), 100); } function obtenerDetalle(pedidoId, callback) { setTimeout(() => callback(null, { pedidoId, items: 3 }), 100); } // ❌ Callback hell — difícil de leer, mantener y depurar obtenerUsuario(1, (err, usuario) => { if (err) return console.error(err); obtenerPedidos(usuario.id, (err, pedidos) => { if (err) return console.error(err); obtenerDetalle(pedidos[0].id, (err, detalle) => { if (err) return console.error(err); console.log(`${usuario.nombre} tiene ${detalle.items} items`); }); }); });
type Usuario = { id: number; nombre: string }; type Pedido = { id: number; total: number }; type Detalle = { pedidoId: number; items: number }; type Callback<T> = (err: Error | null, data: T) => void; function obtenerUsuario(id: number, callback: Callback<Usuario>): void { setTimeout(() => callback(null, { id, nombre: "Ana" }), 100); } function obtenerPedidos(usuarioId: number, callback: Callback<Pedido[]>): void { setTimeout(() => callback(null, [{ id: 1, total: 50 }]), 100); } function obtenerDetalle(pedidoId: number, callback: Callback<Detalle>): void { setTimeout(() => callback(null, { pedidoId, items: 3 }), 100); } // ❌ Callback hell — difícil de leer, mantener y depurar obtenerUsuario(1, (err, usuario) => { if (err) return console.error(err); obtenerPedidos(usuario.id, (err, pedidos) => { if (err) return console.error(err); obtenerDetalle(pedidos[0].id, (err, detalle) => { if (err) return console.error(err); console.log(`${usuario.nombre} tiene ${detalle.items} items`); }); }); });
SalidaAna tiene 3 items
Los síntomas del callback hell

Indentación que crece hacia la derecha, manejo de errores duplicado en cada nivel, y código imposible de reutilizar o testear de forma aislada. Las Promises (lección 06-02) y async/await (06-03) resuelven esto.

Errores en callbacks: la convención Node.js

Node.js popularizó una convención que se volvió estándar: el primer parámetro del callback siempre es el error:

// Convención: callback(error, resultado) // Si hay error, el primer argumento es el Error // Si todo fue bien, el primero es null function dividir(a, b, callback) { if (b === 0) { callback(new Error("No se puede dividir por cero"), null); return; } callback(null, a / b); } dividir(10, 2, (err, resultado) => { if (err) { console.error("Error:", err.message); return; } console.log("Resultado:", resultado); // 5 }); dividir(10, 0, (err, resultado) => { if (err) { console.error("Error:", err.message); // No se puede dividir por cero return; } console.log("Resultado:", resultado); });
// Convención: callback(error, resultado) type CallbackDivision = (err: Error | null, resultado: number | null) => void; function dividir(a: number, b: number, callback: CallbackDivision): void { if (b === 0) { callback(new Error("No se puede dividir por cero"), null); return; } callback(null, a / b); } dividir(10, 2, (err, resultado) => { if (err) { console.error("Error:", err.message); return; } console.log("Resultado:", resultado); // 5 }); dividir(10, 0, (err, resultado) => { if (err) { console.error("Error:", err.message); // No se puede dividir por cero return; } console.log("Resultado:", resultado); });
SalidaResultado: 5 Error: No se puede dividir por cero

¿Cuándo usar callbacks hoy?

Aunque las Promises dominan el código moderno, los callbacks siguen siendo la herramienta correcta en ciertos contextos:

// ✅ Event listeners — la API nativa usa callbacks const boton = document.querySelector("#miBoton"); boton.addEventListener("click", (evento) => { console.log("Clicked:", evento.target.id); }); // ✅ Timers — setTimeout y setInterval const intervalo = setInterval(() => { console.log("Tick"); }, 1000); setTimeout(() => clearInterval(intervalo), 5000); // ✅ APIs de streaming / Node.js import { createReadStream } from "fs"; const stream = createReadStream("datos.txt", "utf8"); stream.on("data", (fragmento) => { console.log("Recibido:", fragmento.length, "bytes"); }); stream.on("end", () => { console.log("Lectura completada"); }); stream.on("error", (err) => { console.error("Error de lectura:", err.message); });
// ✅ Event listeners — la API nativa usa callbacks const boton = document.querySelector<HTMLButtonElement>("#miBoton"); boton?.addEventListener("click", (evento: MouseEvent) => { console.log("Clicked:", (evento.target as HTMLElement).id); }); // ✅ Timers — setTimeout y setInterval const intervalo: ReturnType<typeof setInterval> = setInterval(() => { console.log("Tick"); }, 1000); setTimeout(() => clearInterval(intervalo), 5000); // ✅ APIs de streaming / Node.js import { createReadStream } from "fs"; const stream = createReadStream("datos.txt", "utf8"); stream.on("data", (fragmento: string | Buffer) => { console.log("Recibido:", fragmento.length, "bytes"); }); stream.on("end", () => { console.log("Lectura completada"); }); stream.on("error", (err: Error) => { console.error("Error de lectura:", err.message); });
Callbacks vs Promises hoy

Usa callbacks para event listeners y APIs de streaming nativas. Para lógica de negocio asíncrona nueva, usa siempre Promises o async/await — son más legibles y componibles.

Practica