Programación Web · Capítulo 10
JavaScript Asíncrono: Callbacks, Promesas y Async/Await
Comprende cómo JavaScript maneja operaciones que toman tiempo sin bloquear la interfaz de usuario.
1. ¿Por Qué Existe la Asincronía?
JavaScript es un lenguaje de un solo hilo (single-threaded). Solo puede ejecutar una instrucción a la vez. Sin embargo, las aplicaciones web realizan operaciones que tardan tiempo: cargar datos de un servidor, leer archivos, esperar entradas del usuario. Si JavaScript se bloqueara esperando cada operación lenta, la página quedaría congelada e inutilizable.
La asincronía es la solución: JavaScript inicia una operación lenta, continúa ejecutando otro código, y reacciona cuando la operación termina. Esto es posible gracias al Event Loop.
2. El Event Loop
El Event Loop coordina tres componentes: el Call Stack (pila de ejecución), las Web APIs del navegador, y la Callback Queue (cola de callbacks). Permite que JavaScript sea no bloqueante a pesar de ser single-threaded.
console.log("1. Inicio");
setTimeout(() => {
console.log("3. Timeout completado");
}, 1000);
console.log("2. Código síncrono continúa");
// Salida:
// 1. Inicio
// 2. Código síncrono continúa
// 3. Timeout completado (después de 1 segundo)
El proceso: JavaScript ejecuta el código síncrono línea a línea. Cuando encuentra setTimeout, delega el temporizador al navegador y sigue adelante. Cuando el segundo termina, el callback entra a la cola. El Event Loop lo toma y lo ejecuta cuando el Call Stack está vacío.
3. Callbacks: La Primera Solución
Un callback es simplemente una función que se pasa como argumento a otra función para ser ejecutada después de que ocurra algo.
// Callback simple
function descargarDatos(url, callback) {
setTimeout(() => {
const datos = { usuario: "Ana", edad: 25 }; // Simulación
callback(null, datos); // Convención: primer arg = error, segundo = datos
}, 1000);
}
descargarDatos("https://api.ejemplo.com/usuario", function(error, datos) {
if (error) {
console.error("Error:", error);
return;
}
console.log("Datos recibidos:", datos);
});
El Problema: Callback Hell
Cuando necesitamos operaciones asíncronas en secuencia, los callbacks se anidan y el código se vuelve imposible de mantener:
// Callback Hell — pirámide de la fatalidad
obtenerUsuario(userId, function(err, usuario) {
if (err) { manejarError(err); return; }
obtenerPedidos(usuario.id, function(err, pedidos) {
if (err) { manejarError(err); return; }
obtenerDetalles(pedidos[0].id, function(err, detalles) {
if (err) { manejarError(err); return; }
calcularTotal(detalles, function(err, total) {
if (err) { manejarError(err); return; }
console.log("Total:", total);
// ¡Ya no podemos ver el inicio del código!
});
});
});
});
4. Promesas: Una Mejor Forma
Una Promesa (Promise) es un objeto que representa el resultado eventual de una operación asíncrona. Puede estar en tres estados: pending (pendiente), fulfilled (cumplida) o rejected (rechazada).
// Crear una Promesa
const miPromesa = new Promise((resolve, reject) => {
const exito = true; // Simulamos éxito o fracaso
setTimeout(() => {
if (exito) {
resolve("Operación exitosa"); // Cambia estado a fulfilled
} else {
reject(new Error("Algo salió mal")); // Cambia estado a rejected
}
}, 1000);
});
// Consumir la Promesa
miPromesa
.then(resultado => console.log("Éxito:", resultado))
.catch(error => console.error("Error:", error))
.finally(() => console.log("Siempre se ejecuta"));
Encadenamiento de Promesas
La gran ventaja sobre los callbacks: las promesas se encadenan de forma plana y legible.
function obtenerUsuario(id) {
return new Promise((resolve) => {
setTimeout(() => resolve({ id, nombre: "Ana", ciudadId: 5 }), 500);
});
}
function obtenerCiudad(id) {
return new Promise((resolve) => {
setTimeout(() => resolve({ id, nombre: "Ciudad de México" }), 300);
});
}
// Encadenamiento limpio — no hay pirámide
obtenerUsuario(1)
.then(usuario => {
console.log("Usuario:", usuario.nombre);
return obtenerCiudad(usuario.ciudadId); // Retornar la siguiente promesa
})
.then(ciudad => {
console.log("Ciudad:", ciudad.nombre);
})
.catch(error => {
console.error("Error en cualquier paso:", error);
});
Promise.all y Promise.race
const promesa1 = fetch("https://api.ejemplo.com/usuarios");
const promesa2 = fetch("https://api.ejemplo.com/productos");
const promesa3 = fetch("https://api.ejemplo.com/pedidos");
// Promise.all — espera TODAS, falla si UNA falla
Promise.all([promesa1, promesa2, promesa3])
.then(([usuarios, productos, pedidos]) => {
console.log("Todos los datos cargados");
})
.catch(err => console.error("Al menos una falló:", err));
// Promise.race — resuelve con la PRIMERA que termine
Promise.race([promesa1, promesa2])
.then(resultado => console.log("La más rápida ganó"))
.catch(err => console.error("La más rápida falló"));
5. Async/Await: El Estándar Moderno
Async/await es syntactic sugar sobre las Promesas. Hace que el código asíncrono se vea y se lea como código síncrono.
async marca una función como asíncrona (siempre retorna una Promesa). await pausa la ejecución dentro de la función async hasta que la Promesa se resuelva.
// La misma lógica del encadenamiento, con async/await
async function obtenerInfoUsuario(userId) {
try {
const usuario = await obtenerUsuario(userId);
console.log("Usuario:", usuario.nombre);
const ciudad = await obtenerCiudad(usuario.ciudadId);
console.log("Ciudad:", ciudad.nombre);
return { usuario, ciudad };
} catch (error) {
console.error("Error:", error.message);
throw error; // Re-lanzar si es necesario
}
}
// Llamar a la función async
obtenerInfoUsuario(1)
.then(info => console.log("Completado:", info))
.catch(err => console.error("Falló:", err));
6. La API Fetch: Solicitudes HTTP Reales
fetch() es la forma moderna de hacer solicitudes HTTP en el navegador. Retorna una Promesa.
GET — Obtener Datos
async function obtenerUsuarios() {
try {
// fetch retorna una promesa que resuelve con la respuesta HTTP
const respuesta = await fetch("https://jsonplaceholder.typicode.com/users");
// Verificar que la respuesta fue exitosa
if (!respuesta.ok) {
throw new Error(`Error HTTP: ${respuesta.status}`);
}
// Convertir el cuerpo de la respuesta a JSON
const usuarios = await respuesta.json();
console.log(`Se cargaron ${usuarios.length} usuarios`);
usuarios.forEach(u => console.log(`- ${u.name} (${u.email})`));
return usuarios;
} catch (error) {
console.error("No se pudieron cargar los usuarios:", error);
}
}
obtenerUsuarios();
POST — Enviar Datos
async function crearPublicacion(titulo, cuerpo, userId) {
try {
const respuesta = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer mi-token-aqui"
},
body: JSON.stringify({
title: titulo,
body: cuerpo,
userId: userId
})
});
if (!respuesta.ok) {
throw new Error(`Error al crear: ${respuesta.status}`);
}
const nuevaPublicacion = await respuesta.json();
console.log("Publicación creada con ID:", nuevaPublicacion.id);
return nuevaPublicacion;
} catch (error) {
console.error("Error al crear publicación:", error);
throw error;
}
}
crearPublicacion("Mi título", "Contenido del post", 1);
Ejemplo Real: Cargar y Mostrar Datos
// Función completa con manejo de estado de carga
async function cargarPerfilUsuario(userId) {
const contenedor = document.getElementById("perfil");
contenedor.innerHTML = "Cargando...";
try {
// Cargar usuario y sus publicaciones en paralelo
const [usuarioRes, postsRes] = await Promise.all([
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`),
fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`)
]);
const usuario = await usuarioRes.json();
const posts = await postsRes.json();
contenedor.innerHTML = `
<h2>${usuario.name}</h2>
<p>Email: ${usuario.email}</p>
<p>Ciudad: ${usuario.address.city}</p>
<p>Publicaciones: ${posts.length}</p>
`;
} catch (error) {
contenedor.innerHTML = `Error: ${error.message}`;
}
}
cargarPerfilUsuario(1);
| Característica | Callbacks | Promesas | Async/Await |
| Legibilidad | Baja (anidado) | Media | Alta (parece síncrono) |
| Manejo de errores | Manual en cada nivel | .catch() centralizado | try/catch familiar |
| Operaciones paralelas | Complejo | Promise.all() | await Promise.all() |
| Depuración | Difícil | Media | Fácil (stack traces claros) |
Resumen del Capítulo
- JavaScript es single-threaded; el Event Loop permite operaciones asíncronas sin bloquear el hilo principal delegando a las Web APIs del navegador.
- Los callbacks son la forma original de manejar asincronía, pero generan "callback hell" cuando se anidan múltiples operaciones.
- Las Promesas representan un valor futuro con tres estados (pending, fulfilled, rejected) y permiten encadenar operaciones con
.then() y .catch().
- Promise.all() ejecuta múltiples promesas en paralelo y espera a que todas terminen; Promise.race() resuelve con la primera que termine.
- async/await es syntactic sugar sobre promesas que hace el código asíncrono legible como código síncrono, con manejo de errores via try/catch.
- La API Fetch es el estándar moderno para solicitudes HTTP; siempre verifica
response.ok y maneja errores con try/catch.
- Para operaciones independientes, usar
await Promise.all([]) en lugar de awaits secuenciales para mejorar el rendimiento.