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.
¿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: 14function 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: 1410
Resultado: 14Callbacks 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 timeout1
2
3
Fin del forEach
Antes del timeout
Después del timeout
Dentro del timeoutAunque 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`);
});
});
});Ana tiene 3 itemsIndentació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);
});Resultado: 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);
});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.