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ísticaCallbacksPromesasAsync/Await
LegibilidadBaja (anidado)MediaAlta (parece síncrono)
Manejo de erroresManual en cada nivel.catch() centralizadotry/catch familiar
Operaciones paralelasComplejoPromise.all()await Promise.all()
DepuraciónDifícilMediaFácil (stack traces claros)

Resumen del Capítulo