Tema
Asincronía
Pedir datos a una API, esperar a que el usuario termine de escribir, subir una imagen, mostrar una notificación en 3 segundos... Muchas tareas tardan, y JavaScript no puede quedarse congelado esperándolas. En esta lección entenderás cómo JavaScript hace varias cosas "a la vez" con callbacks, promesas y async/await, y qué es el famoso event loop.
Concepto
Síncrono vs. asíncrono
Imagina una cafetería:
- Síncrona: el cajero toma tu orden, prepara el café, te lo entrega y sólo entonces atiende al siguiente cliente. Si un café tarda 5 minutos, la fila entera espera.
- Asíncrona: el cajero toma tu orden, te da un ticket (una promesa) y atiende al siguiente. Cuando tu café está listo, te llaman por tu nombre.
JavaScript tiene un solo hilo (un solo cajero). Si hiciera las tareas lentas de forma síncrona, la página se congelaría: no podrías hacer clic ni hacer scroll. Por eso las operaciones lentas (red, temporizadores, lectura de archivos) se delegan al navegador y JavaScript sigue con lo demás.
js
console.log('1. Tomo la orden');
setTimeout(() => {
console.log('3. ¡Café listo!');
}, 2000);
console.log('2. Atiendo al siguiente cliente');
// Salida:
// 1. Tomo la orden
// 2. Atiendo al siguiente cliente
// 3. ¡Café listo! (2 segundos después)Temporizadores
js
// Ejecutar una vez después de X milisegundos
const id = setTimeout(() => console.log('Hola'), 1000);
clearTimeout(id); // cancelarlo antes de que ocurra
// Ejecutar cada X milisegundos
let segundos = 0;
const intervalo = setInterval(() => {
segundos++;
console.log(`Han pasado ${segundos} s`);
if (segundos === 5) clearInterval(intervalo); // detenerlo
}, 1000);Error común
setTimeout(fn, 0) no se ejecuta inmediatamente: se ejecuta cuando JavaScript termine todo el código síncrono pendiente. Y el tiempo es un mínimo, no una garantía exacta.
Callbacks
La forma original de manejar la asincronía: pasar una función que se ejecutará cuando la tarea termine.
js
function obtenerUsuario(id, callback) {
setTimeout(() => {
// simulamos una consulta a una base de datos
if (id <= 0) {
callback(new Error('ID inválido'), null);
return;
}
callback(null, { id, nombre: 'Ana' });
}, 1000);
}
obtenerUsuario(1, (error, usuario) => {
if (error) {
console.error(error.message);
return;
}
console.log('Usuario:', usuario.nombre);
});Callback hell
El problema aparece cuando una tarea depende de otra, que depende de otra...
js
obtenerUsuario(1, (err, usuario) => {
if (err) return manejar(err);
obtenerPedidos(usuario.id, (err, pedidos) => {
if (err) return manejar(err);
obtenerDetalle(pedidos[0].id, (err, detalle) => {
if (err) return manejar(err);
calcularEnvio(detalle, (err, envio) => {
if (err) return manejar(err);
console.log('Envío:', envio); // 😵 la "pirámide de la perdición"
});
});
});
});Difícil de leer, de manejar errores y de mantener. Las promesas nacieron para resolver esto.
Promesas
Una promesa es un objeto que representa un valor que estará disponible en el futuro. Es el ticket de la cafetería. Tiene tres estados:
- pending (pendiente): aún esperando.
- fulfilled (cumplida): terminó bien y tiene un valor.
- rejected (rechazada): falló y tiene un error.
Una vez cumplida o rechazada, ya no cambia.
Consumir una promesa: then, catch, finally
js
fetch('https://jsonplaceholder.typicode.com/users/1') // fetch devuelve una promesa
.then((respuesta) => respuesta.json()) // devuelve otra promesa
.then((usuario) => {
console.log(usuario.name);
})
.catch((error) => {
console.error('Algo falló:', error); // captura errores de CUALQUIER paso
})
.finally(() => {
console.log('Terminó (bien o mal)'); // ocultar el "cargando..."
});Cada .then devuelve una nueva promesa, por eso se pueden encadenar en lugar de anidar.
Crear una promesa
js
function esperar(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
esperar(1500).then(() => console.log('Pasaron 1.5 s'));
// Convertir el callback de antes en promesa
function obtenerUsuario(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id <= 0) reject(new Error('ID inválido'));
else resolve({ id, nombre: 'Ana' });
}, 1000);
});
}
obtenerUsuario(1)
.then((usuario) => console.log(usuario))
.catch((error) => console.error(error.message));El callback hell, reescrito con promesas:
js
obtenerUsuario(1)
.then((usuario) => obtenerPedidos(usuario.id))
.then((pedidos) => obtenerDetalle(pedidos[0].id))
.then((detalle) => calcularEnvio(detalle))
.then((envio) => console.log('Envío:', envio))
.catch(manejar); // un solo lugar para los erroresasync / await
async/await es azúcar sintáctica sobre las promesas: te permite escribir código asíncrono que se lee como síncrono.
asyncantes de una función hace que siempre devuelva una promesa.awaitpausa la función (sólo esa función, no la página) hasta que la promesa se resuelva, y te da su valor.
js
async function mostrarEnvio() {
try {
const usuario = await obtenerUsuario(1);
const pedidos = await obtenerPedidos(usuario.id);
const detalle = await obtenerDetalle(pedidos[0].id);
const envio = await calcularEnvio(detalle);
console.log('Envío:', envio);
} catch (error) {
console.error('Error:', error.message);
} finally {
ocultarCargando();
}
}
mostrarEnvio();También funciona con funciones flecha y métodos:
js
const cargarDatos = async () => { /* ... */ };
const api = {
async obtener(url) { /* ... */ }
};Top-level await
Dentro de módulos ES (<script type="module">) puedes usar await directamente, sin función async:
js
const respuesta = await fetch('/api/config');Error común: olvidar el await
js
async function cargar() {
const usuario = obtenerUsuario(1); // falta await
console.log(usuario.nombre); // undefined: usuario es una Promise
}Si ves Promise { <pending> } en la consola, te faltó un await o un .then.
Ejecutar promesas en paralelo
Si las tareas no dependen entre sí, no las esperes una por una: lánzalas al mismo tiempo.
js
// ❌ Secuencial: tarda 1 s + 1 s + 1 s = 3 s
const usuarios = await obtenerUsuarios();
const productos = await obtenerProductos();
const categorias = await obtenerCategorias();
// ✅ En paralelo: tarda ~1 s (lo que tarde la más lenta)
const [usuarios2, productos2, categorias2] = await Promise.all([
obtenerUsuarios(),
obtenerProductos(),
obtenerCategorias()
]);Los combinadores de promesas:
js
// Promise.all: espera a TODAS. Si UNA falla, falla todo.
await Promise.all([p1, p2, p3]);
// Promise.allSettled: espera a todas y te dice cuál salió bien y cuál mal.
const resultados = await Promise.allSettled([p1, p2, p3]);
resultados.forEach((r) => {
if (r.status === 'fulfilled') console.log('OK', r.value);
else console.log('Falló', r.reason);
});
// Promise.race: la PRIMERA que termine (bien o mal) gana. Útil para timeouts.
const conTimeout = (promesa, ms) =>
Promise.race([
promesa,
new Promise((_, reject) => setTimeout(() => reject(new Error('Tiempo agotado')), ms))
]);
// Promise.any: la primera que se CUMPLA. Falla sólo si todas fallan.
const masRapido = await Promise.any([
fetch('https://servidor-1.com/datos'),
fetch('https://servidor-2.com/datos')
]);Buena práctica
Usa Promise.allSettled en dashboards: si el widget de ventas falla, los demás widgets igual deben mostrarse.
El event loop
¿Cómo hace JavaScript, con un solo hilo, para manejar tantas tareas? Gracias al event loop (bucle de eventos). Sus piezas:
- Call stack (pila de llamadas): donde se ejecuta el código, una función a la vez.
- Web APIs: el navegador se encarga de las tareas lentas (temporizadores,
fetch, eventos). - Microtask queue (cola de microtareas): callbacks de promesas (
.then, código después deawait),queueMicrotask. - Task queue (cola de tareas o macrotareas): callbacks de
setTimeout,setInterval, eventos del DOM.
El event loop repite este ciclo:
txt
┌──────────────────────────────────────────────┐
│ 1. Ejecuta TODO el código síncrono (stack) │
│ 2. ¿Stack vacío? Ejecuta TODAS las microtareas│
│ 3. El navegador puede repintar la pantalla │
│ 4. Toma UNA tarea de la task queue │
│ 5. Vuelve al paso 2 │
└──────────────────────────────────────────────┘Las microtareas (promesas) siempre van antes que las tareas (setTimeout). Pon a prueba tu intuición:
js
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
queueMicrotask(() => console.log('D'));
(async () => {
console.log('E');
await null;
console.log('F');
})();
console.log('G');¿Cuál es el orden? (piénsalo antes de abrir)
txt
A ← síncrono
E ← síncrono (una función async se ejecuta síncronamente hasta el primer await)
G ← síncrono
C ← microtarea
D ← microtarea
F ← microtarea (lo que sigue al await)
B ← tarea (setTimeout), al final aunque tenga 0 msExperimenta con el orden de ejecución en este demo:
Error común: bloquear el hilo
Un bucle pesado (procesar 10 millones de elementos) bloquea el stack: la página se congela y no responde a clics. Para cálculos muy pesados usa Web Workers, que corren en otro hilo.
Casos de uso reales
js
// 1. Reintentar una operación que falla (por ejemplo, red inestable)
async function conReintentos(fn, intentos = 3, espera = 1000) {
for (let i = 1; i <= intentos; i++) {
try {
return await fn();
} catch (error) {
if (i === intentos) throw error;
console.warn(`Intento ${i} falló, reintentando...`);
await esperar(espera * i); // espera cada vez más (backoff)
}
}
}
const datos = await conReintentos(() => fetch('/api/datos').then((r) => r.json()));
// 2. Procesar en lotes para no saturar un servidor
async function procesarEnLotes(items, tamanoLote, fn) {
const resultados = [];
for (let i = 0; i < items.length; i += tamanoLote) {
const lote = items.slice(i, i + tamanoLote);
resultados.push(...(await Promise.all(lote.map(fn))));
}
return resultados;
}
// 3. Notificación que desaparece sola
async function mostrarToast(mensaje) {
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = mensaje;
document.body.append(toast);
await esperar(3000);
toast.remove();
}Ejercicios
- Escribe
esperar(ms)y úsala conasync/awaitpara mostrar "3", "2", "1", "¡Despegue!" con un segundo entre cada uno. - Convierte esta función de callback a promesa:
function leer(cb) { setTimeout(() => cb(null, 'datos'), 500); }. - Tienes
obtenerPrecio(producto)que devuelve una promesa. Obtén en paralelo los precios de['mouse', 'teclado', 'monitor']y suma el total. - Escribe
conTimeout(promesa, ms)que rechace con "Tiempo agotado" si la promesa tarda más dems. - Predice el orden:
setTimeout(() => console.log(1)); Promise.resolve().then(() => console.log(2)); console.log(3);
Ver solución
js
// 1
const esperar = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function cuentaRegresiva() {
for (const n of [3, 2, 1]) {
console.log(n);
await esperar(1000);
}
console.log('¡Despegue!');
}
// 2
function leer() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve('datos'), 500);
});
}
// 3
async function totalPrecios() {
const productos = ['mouse', 'teclado', 'monitor'];
const precios = await Promise.all(productos.map(obtenerPrecio));
return precios.reduce((a, b) => a + b, 0);
}
// 4
function conTimeout(promesa, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Tiempo agotado')), ms)
);
return Promise.race([promesa, timeout]);
}
// 5 → 3, 2, 1 (síncrono, microtarea, tarea)Resumen
- JavaScript tiene un solo hilo; las tareas lentas se delegan y su resultado llega después.
- Los callbacks funcionan, pero anidados generan "callback hell".
- Una promesa representa un valor futuro:
pending,fulfilledorejected. Se consume con.then/.catch/.finally. async/awaitpermite escribir código asíncrono legible; maneja errores contry/catch.- Usa
Promise.allpara tareas independientes en paralelo,allSettledcuando algunas pueden fallar yracepara timeouts. - El event loop ejecuta primero el código síncrono, luego todas las microtareas (promesas) y después una tarea (setTimeout).
Siguiente: Fetch y APIs REST