Asincronía, Callbacks y Promesas
Aprende a manejar operaciones asíncronas en JavaScript mediante callbacks, promesas y async/await para crear aplicaciones eficientes y responsivas.
Cristian Escalante
Última actualización: 21 de abril de 2025
Introducción a la programación asíncrona
La programación asíncrona permite que tu código continúe ejecutándose mientras espera que se completen ciertas operaciones, como solicitudes de red, operaciones de archivo o temporizadores. Esto es fundamental en JavaScript, especialmente en entornos como navegadores y Node.js, donde la naturaleza de un solo hilo podría bloquear la interfaz de usuario o el servidor.
Operaciones síncronas vs. asíncronas
Operaciones síncronas:
- Se ejecutan secuencialmente, una después de otra
- Bloquean la ejecución hasta que se completan
- Son predecibles y fáciles de razonar
// Código síncrono
console.log("Inicio");
const resultado = operacionLenta(); // Bloquea hasta completarse
console.log("Resultado:", resultado);
console.log("Fin");
Operaciones asíncronas:
- No bloquean la ejecución del programa
- Permiten que otras operaciones se ejecuten mientras esperan
- Requieren mecanismos especiales para manejar sus resultados
// Código asíncrono
console.log("Inicio");
operacionLentaAsincrona((resultado) => {
console.log("Resultado:", resultado);
});
console.log("Fin"); // Se ejecuta antes de obtener el resultado
Ejemplos de operaciones asíncronas
- Solicitudes de red (AJAX, fetch)
- Operaciones de archivo (en Node.js)
- Temporizadores (setTimeout, setInterval)
- Eventos de usuario (clicks, teclas)
- Animaciones
El event loop de JavaScript
El event loop (bucle de eventos) es el mecanismo que permite a JavaScript manejar operaciones asíncronas a pesar de ser un lenguaje de un solo hilo.
Componentes principales
- Call Stack (Pila de llamadas): Registra la posición actual de ejecución
- Web APIs / Node APIs: Proporcionan funcionalidades asíncronas (setTimeout, fetch, etc.)
- Callback Queue (Cola de callbacks): Almacena callbacks listos para ejecutarse
- Event Loop: Comprueba si la pila está vacía y mueve callbacks de la cola a la pila
Funcionamiento
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
Promise.resolve().then(() => {
console.log("3");
});
console.log("4");
// Salida: 1, 4, 3, 2
Explicación:
console.log("1")
se ejecuta inmediatamentesetTimeout
se registra, pero su callback va a la cola de tareas- La promesa se resuelve y su callback va a la microtask queue
console.log("4")
se ejecuta inmediatamente- La pila se vacía, se ejecutan primero las microtareas (promesa)
- Finalmente se ejecuta el callback de setTimeout
Microtareas vs. Macrotareas
- Microtareas: Promesas, queueMicrotask, MutationObserver
- Macrotareas: setTimeout, setInterval, eventos de UI, I/O
Las microtareas tienen prioridad sobre las macrotareas y se ejecutan inmediatamente después de que la pila se vacía.
Callbacks
Los callbacks son funciones que se pasan como argumentos a otras funciones y se ejecutan después de que ocurra algún evento o se complete una operación.
Definición y uso básico
function procesarDatos(datos, callback) {
// Procesar datos
const resultado = datos.map(x => x * 2);
// Llamar al callback con el resultado
callback(resultado);
}
procesarDatos([1, 2, 3], (resultado) => {
console.log("Resultado procesado:", resultado); // [2, 4, 6]
});
Callbacks en funciones asíncronas
function obtenerDatosDeServidor(id, callback) {
console.log(`Solicitando datos para id: ${id}...`);
// Simular solicitud de red
setTimeout(() => {
const datos = { id: id, nombre: "Producto " + id };
callback(null, datos); // Primer parámetro null indica que no hay error
}, 2000);
}
obtenerDatosDeServidor(123, (error, datos) => {
if (error) {
console.error("Error:", error);
return;
}
console.log("Datos recibidos:", datos);
});
console.log("Continuando ejecución mientras se obtienen los datos...");
Manejo de errores con callbacks
El patrón común en Node.js es pasar el error como primer parámetro:
function dividir(a, b, callback) {
if (b === 0) {
callback(new Error("No se puede dividir por cero"));
return;
}
setTimeout(() => {
callback(null, a / b);
}, 1000);
}
dividir(10, 2, (error, resultado) => {
if (error) {
console.error("Error:", error.message);
return;
}
console.log("Resultado:", resultado); // 5
});
dividir(10, 0, (error, resultado) => {
if (error) {
console.error("Error:", error.message); // "No se puede dividir por cero"
return;
}
console.log("Resultado:", resultado); // No se ejecuta
});
Callback Hell (Infierno de callbacks)
Cuando se anidan múltiples operaciones asíncronas, el código puede volverse difícil de leer y mantener:
obtenerUsuario(userId, (error, usuario) => {
if (error) {
console.error(error);
return;
}
obtenerPermisos(usuario.id, (error, permisos) => {
if (error) {
console.error(error);
return;
}
obtenerPublicaciones(usuario.id, (error, publicaciones) => {
if (error) {
console.error(error);
return;
}
obtenerComentarios(publicaciones[0].id, (error, comentarios) => {
if (error) {
console.error(error);
return;
}
// Código cada vez más anidado...
console.log("Comentarios:", comentarios);
});
});
});
});
Este patrón de código anidado es conocido como "callback hell" o "pirámide de la perdición".
Promesas
Las promesas son objetos que representan la eventual finalización (o fallo) de una operación asíncrona y su valor resultante.
Estados de una promesa
Una promesa puede estar en uno de estos estados:
- Pending (pendiente): Estado inicial, ni cumplida ni rechazada
- Fulfilled (cumplida): La operación se completó con éxito
- Rejected (rechazada): La operación falló
- Settled (resuelta): La promesa ha sido cumplida o rechazada
Creación de promesas
const miPromesa = new Promise((resolve, reject) => {
// Operación asíncrona
const exito = true;
if (exito) {
resolve("¡Operación completada!"); // Cumplir la promesa
} else {
reject(new Error("Algo salió mal")); // Rechazar la promesa
}
});
Consumir promesas con .then() y .catch()
miPromesa
.then(resultado => {
console.log("Éxito:", resultado);
return "Valor procesado: " + resultado;
})
.then(nuevoResultado => {
console.log(nuevoResultado);
})
.catch(error => {
console.error("Error:", error.message);
})
.finally(() => {
console.log("Promesa finalizada (exitosa o no)");
});
Convertir callbacks a promesas
// Función con callback
function obtenerDatosCallback(id, callback) {
setTimeout(() => {
if (id <= 0) {
callback(new Error("ID no válido"));
} else {
callback(null, { id, nombre: "Producto " + id });
}
}, 1000);
}
// Versión con promesas
function obtenerDatos(id) {
return new Promise((resolve, reject) => {
obtenerDatosCallback(id, (error, datos) => {
if (error) {
reject(error);
} else {
resolve(datos);
}
});
});
}
// Uso
obtenerDatos(123)
.then(datos => console.log("Datos:", datos))
.catch(error => console.error("Error:", error.message));
Encadenamiento de promesas
Las promesas permiten encadenar operaciones asíncronas de forma legible:
obtenerUsuario(userId)
.then(usuario => {
console.log("Usuario:", usuario);
return obtenerPermisos(usuario.id);
})
.then(permisos => {
console.log("Permisos:", permisos);
return obtenerPublicaciones(permisos.userId);
})
.then(publicaciones => {
console.log("Publicaciones:", publicaciones);
return obtenerComentarios(publicaciones[0].id);
})
.then(comentarios => {
console.log("Comentarios:", comentarios);
})
.catch(error => {
console.error("Error en la cadena:", error);
});
Métodos de Promise
Promise.all()
Ejecuta múltiples promesas en paralelo y espera a que todas se completen:
const promesa1 = fetch('/api/usuarios');
const promesa2 = fetch('/api/productos');
const promesa3 = fetch('/api/pedidos');
Promise.all([promesa1, promesa2, promesa3])
.then(([resUsuarios, resProductos, resPedidos]) => {
// Todas las promesas se completaron exitosamente
return Promise.all([
resUsuarios.json(),
resProductos.json(),
resPedidos.json()
]);
})
.then(([usuarios, productos, pedidos]) => {
console.log({ usuarios, productos, pedidos });
})
.catch(error => {
// Si cualquiera falla, se ejecuta el catch
console.error("Error en alguna solicitud:", error);
});
Promise.race()
Devuelve la primera promesa que se resuelva (ya sea cumplida o rechazada):
const promesaRapida = new Promise(resolve => setTimeout(() => resolve("Rápida"), 500));
const promesaLenta = new Promise(resolve => setTimeout(() => resolve("Lenta"), 1000));
Promise.race([promesaRapida, promesaLenta])
.then(resultado => {
console.log("La ganadora fue:", resultado); // "Rápida"
});
// Ejemplo práctico: timeout para una solicitud
function solicitarConTimeout(url, ms) {
const promesaFetch = fetch(url).then(res => res.json());
const promesaTimeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), ms)
);
return Promise.race([promesaFetch, promesaTimeout]);
}
solicitarConTimeout('/api/datos', 5000)
.then(datos => console.log(datos))
.catch(error => console.error(error.message));
Promise.any() (ES2021)
Devuelve la primera promesa que se cumpla (ignora rechazos):
const promesa1 = new Promise((resolve, reject) => setTimeout(() => reject(new Error("Error 1")), 1000));
const promesa2 = new Promise(resolve => setTimeout(() => resolve("Éxito 2"), 2000));
const promesa3 = new Promise(resolve => setTimeout(() => resolve("Éxito 3"), 3000));
Promise.any([promesa1, promesa2, promesa3])
.then(resultado => {
console.log("Primera en tener éxito:", resultado); // "Éxito 2"
})
.catch(error => {
console.error("Todas fallaron:", error);
});
Promise.allSettled() (ES2020)
Espera a que todas las promesas se resuelvan (cumplidas o rechazadas) y devuelve un array con los resultados:
const promesas = [
Promise.resolve("éxito 1"),
Promise.reject(new Error("error")),
Promise.resolve("éxito 2")
];
Promise.allSettled(promesas)
.then(resultados => {
console.log(resultados);
// [
// { status: "fulfilled", value: "éxito 1" },
// { status: "rejected", reason: Error: "error" },
// { status: "fulfilled", value: "éxito 2" }
// ]
const exitosos = resultados
.filter(r => r.status === "fulfilled")
.map(r => r.value);
console.log("Resultados exitosos:", exitosos);
});
Async/Await
Async/await es una sintaxis que facilita el trabajo con promesas, haciendo que el código asíncrono se vea y se comporte más como código síncrono.
Funciones async
Una función async siempre devuelve una promesa:
async function obtenerDatos() {
return "Datos obtenidos"; // Se envuelve automáticamente en Promise.resolve()
}
obtenerDatos().then(console.log); // "Datos obtenidos"
// Equivalente a:
function obtenerDatosPromesa() {
return Promise.resolve("Datos obtenidos");
}
Palabra clave await
La palabra clave await
pausa la ejecución de una función async hasta que una promesa se resuelva:
async function mostrarUsuario(id) {
try {
console.log("Obteniendo usuario...");
const usuario = await obtenerUsuario(id); // Espera a que la promesa se resuelva
console.log("Obteniendo permisos...");
const permisos = await obtenerPermisos(usuario.id);
console.log("Usuario:", usuario);
console.log("Permisos:", permisos);
return { usuario, permisos };
} catch (error) {
console.error("Error:", error);
throw error; // Propagar el error
}
}
// Llamar a la función async
mostrarUsuario(123)
.then(resultado => console.log("Todo completado:", resultado))
.catch(error => console.error("Error general:", error));
Ejecución paralela con async/await
async function obtenerDatosParalelo() {
try {
// Iniciar ambas solicitudes en paralelo
const promesaUsuarios = obtenerUsuarios();
const promesaProductos = obtenerProductos();
// Esperar a que ambas terminen
const usuarios = await promesaUsuarios;
const productos = await promesaProductos;
return { usuarios, productos };
} catch (error) {
console.error("Error:", error);
throw error;
}
}
// Alternativa usando Promise.all
async function obtenerDatosParaleloAll() {
try {
const [usuarios, productos] = await Promise.all([
obtenerUsuarios(),
obtenerProductos()
]);
return { usuarios, productos };
} catch (error) {
console.error("Error:", error);
throw error;
}
}
Bucles con async/await
// Secuencial (cada iteración espera a la anterior)
async function procesarSecuencial(items) {
const resultados = [];
for (const item of items) {
const resultado = await procesarItem(item); // Espera antes de la siguiente iteración
resultados.push(resultado);
}
return resultados;
}
// Paralelo (todas las promesas se inician a la vez)
async function procesarParalelo(items) {
const promesas = items.map(item => procesarItem(item));
return Promise.all(promesas);
}
Manejo de errores en código asíncrono
Try/catch con async/await
async function obtenerDatosUsuario(id) {
try {
const usuario = await obtenerUsuario(id);
const publicaciones = await obtenerPublicaciones(usuario.id);
return { usuario, publicaciones };
} catch (error) {
console.error("Error al obtener datos:", error);
// Puedes manejar diferentes tipos de errores
if (error.name === "NotFoundError") {
return { error: "Usuario no encontrado" };
}
throw new Error("No se pudieron obtener los datos: " + error.message);
} finally {
console.log("Operación finalizada");
}
}
Errores en promesas encadenadas
obtenerUsuario(id)
.then(usuario => {
if (!usuario.activo) {
throw new Error("Usuario inactivo");
}
return obtenerPermisos(usuario.id);
})
.then(permisos => {
if (permisos.nivel < 5) {
throw new Error("Permisos insuficientes");
}
return obtenerDatos(permisos.nivel);
})
.catch(error => {
// Maneja todos los errores de la cadena
console.error("Error en el proceso:", error.message);
// Puedes determinar dónde ocurrió el error
if (error.message === "Usuario inactivo") {
// Manejar específicamente
}
});
Errores no capturados
Las promesas rechazadas no capturadas pueden causar problemas:
// Escuchar errores no capturados en el navegador
window.addEventListener('unhandledrejection', event => {
console.error('Promesa rechazada no capturada:', event.promise, 'Razón:', event.reason);
event.preventDefault(); // Prevenir el comportamiento predeterminado
});
// En Node.js
process.on('unhandledRejection', (reason, promise) => {
console.error('Promesa rechazada no capturada:', promise, 'Razón:', reason);
});
Patrones y mejores prácticas
Promisificación
Convertir APIs basadas en callbacks a promesas:
// Función de utilidad para promisificar
function promisify(fn) {
return function(...args) {
return new Promise((resolve, reject) => {
fn(...args, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
};
}
// Ejemplo de uso
const fs = require('fs');
const readFile = promisify(fs.readFile);
// Ahora puedes usar async/await
async function leerArchivo() {
try {
const contenido = await readFile('archivo.txt', 'utf8');
console.log(contenido);
} catch (error) {
console.error('Error al leer archivo:', error);
}
}
Manejo de concurrencia
async function procesarLotes(items, tamañoLote = 5) {
const resultados = [];
// Procesar en lotes para controlar la concurrencia
for (let i = 0; i < items.length; i += tamañoLote) {
const lote = items.slice(i, i + tamañoLote);
const resultadosLote = await Promise.all(
lote.map(item => procesarItem(item))
);
resultados.push(...resultadosLote);
// Opcional: esperar un poco entre lotes
await new Promise(resolve => setTimeout(resolve, 100));
}
return resultados;
}
Cancelación de promesas
JavaScript no tiene un mecanismo nativo para cancelar promesas, pero puedes implementarlo:
function obtenerDatosCancelable() {
let cancelado = false;
const promesa = new Promise((resolve, reject) => {
const id = setTimeout(() => {
if (!cancelado) {
resolve({ datos: "Información obtenida" });
}
}, 5000);
// Función para limpiar si se cancela
promesa.cancel = () => {
cancelado = true;
clearTimeout(id);
reject(new Error("Operación cancelada"));
};
});
return promesa;
}
// Uso
const peticion = obtenerDatosCancelable();
peticion.then(console.log).catch(console.error);
// Para cancelar:
setTimeout(() => {
peticion.cancel();
}, 2000);
AbortController (API moderna)
async function obtenerDatosConTimeout(url, tiempoLimite) {
const controller = new AbortController();
const { signal } = controller;
// Configurar timeout
const timeoutId = setTimeout(() => controller.abort(), tiempoLimite);
try {
const respuesta = await fetch(url, { signal });
clearTimeout(timeoutId); // Limpiar timeout si la solicitud tiene éxito
return await respuesta.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error(`La solicitud excedió el tiempo límite de ${tiempoLimite}ms`);
}
throw error;
}
}
// Uso
obtenerDatosConTimeout('https://api.ejemplo.com/datos', 3000)
.then(datos => console.log('Datos:', datos))
.catch(error => console.error('Error:', error.message));
Resumen
- Callbacks: Funciones pasadas como argumentos que se ejecutan después de completar una operación
- Promesas: Objetos que representan un valor futuro, con estados (pendiente, cumplida, rechazada)
- Async/await: Sintaxis que facilita el trabajo con promesas, haciendo el código asíncrono más legible
- Métodos de Promise:
Promise.all()
,Promise.race()
,Promise.any()
,Promise.allSettled()
- Manejo de errores:
try/catch
con async/await,.catch()
con promesas
La programación asíncrona es fundamental en JavaScript para crear aplicaciones responsivas y eficientes. Dominar estos conceptos te permitirá manejar operaciones como solicitudes de red, operaciones de E/S y temporizadores de manera efectiva.