JavaScript
Callbacks
Explica qué es un callback, quién controla su ejecución y cómo cambia su contrato en arrays, eventos, temporizadores, listeners y APIs asíncronas.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
- Fundamentos de programación
JavaScript
Explica qué es un callback, quién controla su ejecución y cómo cambia su contrato en arrays, eventos, temporizadores, listeners y APIs asíncronas.
Un callback es una función que se entrega a otra función para que esta decida cuándo, cómo y con qué argumentos ejecutarla.
function greet(name) {
console.log(`Hello, ${name}`);
}
function processUser(name, callback) {
callback(name);
}
processUser("Nicolás", greet);La definición de callback no dice que deba ejecutarse después ni esperar una operación.
Este callback es síncrono:
const numbers = [1, 2, 3];
const doubled = numbers.map((number) => number * 2);map llama el callback durante la ejecución actual y no termina hasta procesar el array.
Este callback se ejecuta más adelante:
setTimeout(() => {
console.log("Later");
}, 1000);La diferencia depende de la función o API receptora, no de que la función se llame callback.
button.addEventListener("click", handleClick);Aquí se entrega la función para que el navegador la ejecute cuando ocurra el clic.
button.addEventListener("click", handleClick());Aquí handleClick se ejecuta inmediatamente y se entrega su valor de retorno como segundo argumento. Normalmente no es lo deseado.
Quien recibe el callback decide:
function repeat(times, callback) {
for (let index = 0; index < times; index++) {
callback(index);
}
}
repeat(3, (index) => {
console.log(index);
});repeat llama tres veces y entrega el índice actual.
const activeProducts = products.filter(
(product, index, array) => product.isActive,
);filter puede entregar al callback:
No es necesario declarar parámetros que no se utilicen.
const names = users.map((user) => user.name);El retorno también depende del método:
map utiliza el retorno como nuevo elemento.filter interpreta el retorno como condición.find detiene la búsqueda cuando el retorno es truthy.forEach ignora el retorno.function handleSubmit(event) {
event.preventDefault();
console.log("Form submitted");
}
form.addEventListener("submit", handleSubmit);El navegador llama handleSubmit y entrega un objeto de evento.
Para eliminar un listener se necesita la misma función.
function handleClick() {
console.log("Clicked");
}
button.addEventListener("click", handleClick);
button.removeEventListener("click", handleClick);Esto no funciona:
button.addEventListener("click", () => console.log("Clicked"));
button.removeEventListener("click", () => console.log("Clicked"));Las dos arrow functions son valores diferentes.
console.log("A");
setTimeout(() => {
console.log("B");
}, 0);
console.log("C");Resultado:
A
C
BEl temporizador registra el callback para una ejecución posterior. 0 no significa “ejecutar ahora”; significa que no debe esperarse un retraso adicional una vez que el entorno pueda programarlo.
El Event Loop explicará cómo llega finalmente al Call Stack.
function createClickHandler(productId) {
return function handleClick() {
openProduct(productId);
};
}
const handler = createClickHandler("product-123");
button.addEventListener("click", handler);El callback conserva acceso a productId mediante un closure.
Algunas APIs antiguas reciben callbacks separados.
loadProducts(
(products) => {
renderProducts(products);
},
(error) => {
showError(error);
},
);O utilizan la convención error-first callback, frecuente en APIs tradicionales de Node.js.
readFile("data.json", (error, content) => {
if (error) {
console.error(error);
return;
}
console.log(content);
});El primer argumento representa el error y el segundo el resultado.
Un retorno termina el callback, no la función exterior.
function findActiveUser(users) {
users.forEach((user) => {
if (user.isActive) {
return user;
}
});
return null;
}La función exterior siempre devuelve null.
Para una búsqueda, utiliza el contrato adecuado:
function findActiveUser(users) {
return users.find((user) => user.isActive) ?? null;
}Cuando entregas un callback a otra función, también le entregas parte del control.
thirdPartyOperation(() => {
chargeCustomer();
});Estás confiando en que thirdPartyOperation:
Esta idea se conoce como inversión de control. No hace malos a los callbacks, pero vuelve importante conocer el contrato de la API receptora.
Cuando varias operaciones dependen unas de otras, los callbacks anidados pueden ocultar el flujo.
getUser(userId, (userError, user) => {
if (userError) return handleError(userError);
getOrders(user.id, (ordersError, orders) => {
if (ordersError) return handleError(ordersError);
getPayment(orders[0].id, (paymentError, payment) => {
if (paymentError) return handleError(paymentError);
confirmPayment(payment);
});
});
});El problema no son los callbacks aislados, sino la combinación de anidamiento, dependencias y manejo repetido de errores.
Promesas y async/await ofrecen otra forma de representar secuencias asíncronas. Los callbacks siguen siendo fundamentales en eventos, métodos de arrays y muchas APIs.
Un try...catch exterior no captura necesariamente un error lanzado después.
try {
setTimeout(() => {
throw new Error("Failed");
}, 0);
} catch (error) {
console.log("This does not catch it");
}Cuando el callback se ejecuta, la llamada que creó el temporizador ya terminó. El error ocurre en una ejecución nueva.
El manejo correcto depende de la API: callback de error, promesa rechazada, listener de error u otro mecanismo.
Este contrato es difícil de entender:
processOrder(order, true, resultHandler);Una interfaz más clara puede utilizar opciones con nombres:
processOrder(order, {
reserveStock: true,
onComplete: resultHandler,
});Nombres como onSuccess, onError, onComplete, predicate, transform o compare comunican el papel del callback.
function processProducts(products, predicate, transform) {
const result = [];
for (const product of products) {
if (!predicate(product)) {
continue;
}
result.push(transform(product));
}
return result;
}
const summaries = processProducts(
products,
(product) => product.isActive,
(product) => ({
id: product.id,
name: product.name,
price: product.price,
}),
);processProducts controla el recorrido. Los callbacks personalizan la condición y la transformación.
setTimeout(..., 0) ejecuta inmediatamente.¿Cuáles callbacks se ejecutan de forma síncrona?
[1, 2, 3].map((number) => number * 2);
setTimeout(() => console.log("Timer"), 0);
button.addEventListener("click", () => console.log("Click"));El callback de map se ejecuta síncronamente durante la llamada. El del temporizador se programa para después y el del evento solo se ejecuta cuando ocurre un clic.
Funciones de orden superior generaliza la capacidad de recibir o devolver funciones para construir comportamientos reutilizables.