Asincronía en JavaScript
En esta lección
Algunas operaciones tardan: pedir datos a un servidor, esperar a que el usuario pulse un botón, leer un archivo, esperar dos segundos. Si JavaScript se quedara parado esperando, la página se congelaría. Por eso existe la asincronía: lanzar una tarea, seguir con otras cosas y reaccionar cuando termine. En esta lección vas a entender cómo lo consigue JavaScript con un solo hilo (el event loop), y las tres formas de escribir código asíncrono: callbacks, promesas y async/await.
Para pedir datos a un servidor en la práctica, después de esta lección tienes fetch, promesas y async/await. Los temporizadores los viste en setTimeout y setInterval.
Síncrono frente a asíncrono
El código síncrono se ejecuta línea a línea, y cada una espera a que termine la anterior:
console.log("A");
console.log("B");
console.log("C");
// A, B, C
Una operación asíncrona se lanza ahora y termina más tarde. Mientras tanto, el programa sigue:
console.log("A");
setTimeout(() => console.log("B"), 1000);
console.log("C");
// A, C y, un segundo después, B
Piensa en una cafetería. Un camarero síncrono tomaría tu pedido, se quedaría mirando la cafetera hasta que el café esté hecho y solo entonces atendería al siguiente cliente. Un camarero asíncrono toma tu pedido, lo pasa a la barra y atiende al siguiente; cuando el café está listo, te avisan. JavaScript es el segundo camarero.
Un solo hilo y el event loop
JavaScript ejecuta tu código en un solo hilo: solo puede hacer una cosa a la vez. Entonces, ¿cómo espera a un servidor sin bloquearse? Porque no es él quien espera. El navegador (o Node) se encarga de los temporizadores, las peticiones de red y los eventos, y cuando uno termina, deja su callback en una cola. El event loop (bucle de eventos) es el mecanismo que, cada vez que tu código termina, mira las colas y ejecuta lo siguiente.
Las piezas son:
- La pila de llamadas (call stack): el código que se está ejecutando ahora.
- La cola de microtareas: callbacks de promesas (
then,catch,await). Tienen prioridad. - La cola de tareas (macrotareas): callbacks de
setTimeout,setInterval, eventos del usuario, etc.
La regla del event loop: cuando la pila se vacía, se ejecutan TODAS las microtareas pendientes; después, UNA tarea; y vuelta a empezar.
El ejemplo que lo explica todo
console.log("1. Inicio");
setTimeout(() => console.log("4. setTimeout con 0 ms"), 0);
Promise.resolve().then(() => console.log("3. then (microtarea)"));
console.log("2. Fin del código síncrono");
Salida:
1. Inicio
2. Fin del código síncrono
3. then (microtarea)
4. setTimeout con 0 ms
Paso a paso:
- Se imprime “1”.
setTimeoutdeja su callback en la cola de tareas (aunque sea con 0 ms, nunca se ejecuta al momento).thendeja su callback en la cola de microtareas.- Se imprime “2”. La pila queda vacía.
- El event loop ejecuta todas las microtareas: “3”.
- Después, una tarea: “4”.
Consecuencia práctica: un bucle síncrono muy largo bloquea todo. Mientras se ejecuta, no se procesa ningún clic ni ningún temporizador, y la página parece colgada.
Callbacks y el callback hell
La primera forma de trabajar con asincronía fue pasar un callback que se llama al terminar (repasa callbacks si lo necesitas):
function cargarUsuario(id, callback) {
setTimeout(() => {
callback({ id, nombre: "Ana" });
}, 500);
}
cargarUsuario(7, (usuario) => {
console.log(usuario.nombre);
});
Funciona bien con una sola operación. El problema llega cuando una depende de otra, y esa de otra más:
cargarUsuario(7, (usuario) => {
cargarPedidos(usuario, (pedidos) => {
cargarDetalle(pedidos[0], (detalle) => {
calcularEnvio(detalle, (envio) => {
console.log(envio);
// y cada nivel necesita, además, su gestión de errores...
});
});
});
});
Esta pirámide que crece hacia la derecha se llama callback hell (el infierno de los callbacks). Es difícil de leer, de mantener y, sobre todo, de gestionar los errores. Las promesas nacieron para resolverlo.
Promesas
Una promesa es un objeto que representa un resultado que llegará en el futuro. Tiene tres estados:
| Estado | Significa |
|---|---|
| pending (pendiente) | Todavía no ha terminado |
| fulfilled (cumplida) | Terminó bien y tiene un valor |
| rejected (rechazada) | Falló y tiene un motivo (normalmente un Error) |
Una vez cumplida o rechazada, ya no cambia.
Crear una promesa: new Promise
Normalmente usarás promesas que te dan otras funciones (fetch, por ejemplo), pero crear una te ayuda a entenderlas. El constructor recibe una función con dos parámetros, resolve y reject:
function pedirPizza(masa) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (masa === "sin gluten") {
reject(new Error("No queda masa sin gluten"));
} else {
resolve(`Pizza con masa ${masa}`);
}
}, 500);
});
}
resolve(valor)cumple la promesa con ese valor.reject(error)la rechaza. Pasa siempre unError, no un texto, para tener la traza.
Un caso muy útil: una función para esperar.
const esperar = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
Consumir una promesa: then, catch y finally
pedirPizza("fina")
.then((pizza) => console.log("Llega:", pizza))
.catch((error) => console.log("Error:", error.message))
.finally(() => console.log("Pedido cerrado"));
// Llega: Pizza con masa fina
// Pedido cerrado
pedirPizza("sin gluten")
.then((pizza) => console.log("Llega:", pizza))
.catch((error) => console.log("Error:", error.message))
.finally(() => console.log("Pedido cerrado"));
// Error: No queda masa sin gluten
// Pedido cerrado
thense ejecuta si se cumple y recibe el valor.catchse ejecuta si se rechaza y recibe el error.finallyse ejecuta siempre, útil para quitar un “Cargando…”.
Encadenar promesas
then devuelve una promesa nueva con lo que devuelva su callback. Si devuelves otra promesa, el siguiente then espera a que se cumpla. Así la pirámide se convierte en una lista:
buscarUsuario(7)
.then((usuario) => buscarPedidos(usuario))
.then((pedidos) => pedidos.reduce((total, p) => total + p.total, 0))
.then((total) => console.log("Total gastado:", total))
.catch((error) => console.log("Algo falló:", error.message));
// Total gastado: 45
Un solo catch al final recoge el error de cualquier paso anterior.
Cuidado: dentro de un
then, devuelve la promesa (returno flecha sin llaves). Si escribes.then((u) => { buscarPedidos(u); }), el siguientethenno espera y recibeundefined.
Varias promesas a la vez
Cuando tienes varias operaciones independientes, lánzalas juntas y espera el conjunto. Hay cuatro combinadores:
| Método | Se cumple cuando… | Se rechaza cuando… |
|---|---|---|
Promise.all | todas se cumplen (array de valores, en orden) | una falla |
Promise.allSettled | todas terminan, bien o mal | nunca |
Promise.race | la primera que termine se cumple | la primera que termine falla |
Promise.any | la primera que se cumpla | todas fallan (AggregateError) |
const [usuario, pedidos] = await Promise.all([
cargarUsuario(7),
cargarPedidos(7),
]);
allSettled es útil cuando quieres saber qué ha ido bien y qué mal sin que un fallo lo anule todo:
const resultados = await Promise.allSettled([tarea1, tarea2]);
// [ { status: 'fulfilled', value: 'A' },
// { status: 'rejected', reason: Error: B falla } ]
Y race sirve, por ejemplo, para poner un tiempo máximo:
const limite = (ms) =>
new Promise((_, reject) => setTimeout(() => reject(new Error("Tiempo agotado")), ms));
await Promise.race([cargarDatos(), limite(5000)]);
async y await
async/await es una forma de escribir código con promesas que parece síncrono. Por debajo son las mismas promesas, pero se lee de arriba abajo:
async function totalGastado(id) {
try {
const usuario = await buscarUsuario(id);
const pedidos = await buscarPedidos(usuario);
return pedidos.reduce((total, p) => total + p.total, 0);
} catch (error) {
console.log("Algo falló:", error.message);
return 0;
}
}
awaitpausa esa función (no el programa entero) hasta que la promesa se resuelve, y te da su valor. Si se rechaza, lanza el error.- Por eso los errores se capturan con
try...catch, como cualquier otro (lo tienes en errores y excepciones). - Una función
asyncsiempre devuelve una promesa. Quien la llame tendrá que usarawaitothen.
Compara con la versión de then: hace exactamente lo mismo. Usa async/await por defecto; es más fácil de leer y depurar.
await en bucles: secuencial o en paralelo
Aquí se pierde mucho rendimiento sin darse cuenta:
const ids = [1, 2, 3];
// Secuencial: espera a cada una antes de lanzar la siguiente (~300 ms)
for (const id of ids) {
const producto = await cargarProducto(id); // cada una tarda 100 ms
}
// En paralelo: las lanza todas y espera el conjunto (~100 ms)
const productos = await Promise.all(ids.map((id) => cargarProducto(id)));
Usa el bucle secuencial solo cuando cada paso necesita el resultado del anterior o no quieres saturar el servidor. Si son independientes, Promise.all.
Cuidado:
forEachno espera a los callbacksasync.ids.forEach(async (id) => await ...)lanza todo y sigue sin esperar. Usafor...of(secuencial) oPromise.allconmap(paralelo).
Top-level await
En los módulos (<script type="module"> o archivos .mjs en Node) puedes usar await fuera de una función:
// datos.mjs
const respuesta = await fetch("/api/config");
export const config = await respuesta.json();
En un script normal no se puede: tienes que envolverlo en una función async.
Errores frecuentes
- Olvidar el
await: obtienesPromise { <pending> }en lugar del valor. - Usar
awaitfuera de una funciónasync(y fuera de un módulo):SyntaxError. - No devolver la promesa en un
then: la cadena no espera. - No capturar los errores: una promesa rechazada sin
catchaparece como Uncaught (in promise). awaitdentro deforEach: no espera.- Encadenar
awaitque no dependen entre sí: lento sin necesidad; usaPromise.all. - Esperar que
setTimeout(fn, 0)se ejecute al momento: siempre va después del código síncrono y de las microtareas.
Resumen
| Concepto | Idea clave |
|---|---|
| Asíncrono | Se lanza ahora, termina después; el programa sigue |
| Event loop | Pila vacía → todas las microtareas → una tarea |
| Microtareas | then, catch, await: van antes que setTimeout |
| Callback | Función que se llama al terminar; anidados = callback hell |
new Promise((resolve, reject) => ...) | Crear una promesa |
then / catch / finally | Valor, error, siempre |
Promise.all / allSettled / race / any | Combinar varias |
async / await | Promesas escritas como código secuencial |
try...catch | Capturar errores con await |
Promise.all(lista.map(...)) | Varias en paralelo |
Pruébalo tú
Cambia el código y pulsa Ejecutar (o Ctrl + Enter).
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.