Módulo 12: Asincronía
Objetivos del módulo
- Entender la diferencia entre código síncrono y asíncrono, y por qué JavaScript necesita la asincronía
- Comprender cómo funciona el event loop: pila de llamadas, APIs del entorno, macrotareas y microtareas
- Usar temporizadores (
setTimeout,setInterval) y cancelarlos - Reconocer los callbacks y el callback hell
- Dominar las promesas: estados,
then/catch/finally, encadenamiento y promisificación - Combinar promesas con
Promise.all,allSettled,anyyrace, y conocerPromise.withResolversyPromise.try - Escribir código asíncrono legible con
async/awaity gestionar sus errores - Distinguir ejecución secuencial y en paralelo, y evitar los errores típicos de
awaiten bucles - Usar top-level await, generadores asíncronos,
for await...ofyArray.fromAsync(ES2026) - Cancelar operaciones con
AbortControlleryAbortSignal - Detectar promesas rechazadas que nadie gestiona
1. Síncrono frente a asíncrono
📘 Concepto: Un código es síncrono cuando cada instrucción espera a que termine la anterior. Es asíncrono cuando lanzamos una operación lenta (descargar un fichero, esperar un temporizador, leer un disco) y seguimos haciendo otras cosas mientras tanto; cuando la operación termina, se nos avisa para continuar.
La analogía del restaurante
Imagina un restaurante con un único camarero:
- Camarero síncrono: toma la comanda de la mesa 1, va a la cocina y se queda de pie esperando los 20 minutos que tarda el plato. Mientras tanto, las mesas 2, 3 y 4 no son atendidas. El restaurante parece “congelado”.
- Camarero asíncrono: toma la comanda de la mesa 1, la deja en la cocina y vuelve a la sala a atender a la mesa 2, a la 3… Cuando el cocinero toca la campana (“¡plato listo!”), el camarero lo recoge y lo sirve.
JavaScript es ese camarero: tiene un solo hilo de ejecución (un único camarero), pero gracias a la asincronía puede atender muchas cosas sin quedarse bloqueado. La cocina son las APIs del entorno (el navegador o Node.js), que sí pueden trabajar en paralelo; la campana es el aviso que deja una tarea pendiente en una cola.
Por qué importa en la web
En el navegador, el mismo hilo que ejecuta tu JavaScript es el que pinta la página y responde a los clics. Si lo bloqueas, la página se congela:
// ❌ Bloqueo síncrono: durante 3 segundos la pestaña no responde (espera activa)
const fin = Date.now() + 3000;
while (Date.now() < fin) {}
console.log('Por fin libre');
// ✅ Asíncrono: programamos el aviso y el hilo queda libre
console.log('Pido el plato');
setTimeout(() => console.log('Plato servido (3 s después)'), 3000);
console.log('Mientras tanto, atiendo otras mesas');
// Salida: Pido el plato → Mientras tanto, atiendo otras mesas → Plato servido (3 s después)
⚠️ Importante: Asíncrono no significa “en paralelo dentro de tu código”. Tu JavaScript sigue ejecutándose en un solo hilo, de una instrucción en una. Lo que ocurre en paralelo es el trabajo del entorno (la red, el temporizador, el disco). Para paralelismo real de CPU existen los Web Workers, que verás en el módulo 18.
2. Un solo hilo y el event loop
Para entender por qué el código asíncrono se ejecuta en el orden en que lo hace, hay que conocer las piezas del motor.
| Pieza | Qué es | Analogía |
|---|---|---|
| Pila de llamadas (call stack) | Las funciones que se están ejecutando ahora mismo, una encima de otra | Lo que el camarero tiene entre manos |
| APIs del entorno (Web APIs en el navegador, libuv en Node) | Temporizadores, red, disco, eventos del DOM… trabajan fuera de tu hilo | La cocina |
| Cola de macrotareas (task queue) | Callbacks listos para ejecutarse: temporizadores, eventos, mensajes | Platos listos en el pase |
| Cola de microtareas (microtask queue) | Callbacks de promesas (then, await) y queueMicrotask | Notas urgentes que el camarero lee antes de coger otro plato |
| Event loop | El bucle que decide qué se ejecuta a continuación | El jefe de sala |
Las reglas del event loop
- Ejecuta el código síncrono hasta que la pila quede vacía.
- Vacía por completo la cola de microtareas (si una microtarea añade otra, también se ejecuta en esta ronda).
- El navegador puede repintar la pantalla si hace falta.
- Coge una sola macrotarea de su cola y la ejecuta (vuelve al paso 1).
flowchart LR
subgraph Motor["Motor de JavaScript (un hilo)"]
Pila["Pila de llamadas"]
end
subgraph Entorno["APIs del entorno"]
T["Temporizadores"]
R["Red (fetch)"]
E["Eventos del DOM"]
end
Pila -- "setTimeout, fetch, addEventListener" --> Entorno
T -- "callback listo" --> Macro["Cola de macrotareas"]
E -- "callback listo" --> Macro
R -- "respuesta: resuelve la promesa" --> Micro["Cola de microtareas"]
Pila -- "then / await / queueMicrotask" --> Micro
Loop(("Event loop"))
Micro -- "1º: todas" --> Loop
Macro -- "2º: una" --> Loop
Loop -- "cuando la pila está vacía" --> Pila
El orden de los console.log
💻 Ejemplo: Este programa es el clásico de las entrevistas. Antes de leer la salida, intenta adivinar el orden. Guárdalo como
orden.mjsy ejecútalo connode orden.mjs(o pégalo en la consola del navegador).
console.log('1. Inicio (síncrono)');
setTimeout(() => console.log('6. setTimeout 0 (macrotarea)'), 0);
Promise.resolve().then(() => console.log('4. then de una promesa (microtarea)'));
queueMicrotask(() => console.log('5. queueMicrotask (microtarea)'));
(async () => {
console.log('2. Dentro de una función async (síncrono hasta el primer await)');
await null;
console.log('5b. Después del await (microtarea)');
})();
console.log('3. Fin (síncrono)');
Salida real (Node.js 24):
1. Inicio (síncrono)
2. Dentro de una función async (síncrono hasta el primer await)
3. Fin (síncrono)
4. then de una promesa (microtarea)
5. queueMicrotask (microtarea)
5b. Después del await (microtarea)
6. setTimeout 0 (macrotarea)
¿Por qué?
- Las líneas 1, 2 y 3 son síncronas: se ejecutan en cuanto se leen. Fíjate en que el cuerpo de una función
asyncse ejecuta de forma síncrona hasta el primerawait. - El
then, elqueueMicrotasky la continuación tras elawaitson microtareas: se encolan en orden y se ejecutan en cuanto la pila queda vacía. - El
setTimeout(..., 0)es una macrotarea: aunque el retraso sea 0, tiene que esperar a que se vacíen todas las microtareas.
⚠️ Importante:
setTimeout(fn, 0)no significa “ahora mismo”, sino “lo antes posible, cuando el hilo quede libre y no haya microtareas pendientes”. El retraso que indicas es un mínimo, nunca una garantía.
💡 Consejo: Como las microtareas se vacían por completo antes de pasar a la siguiente macrotarea, un bucle infinito de microtareas (una que encola otra sin fin) congela la página igual que un
while (true).
3. Temporizadores
setTimeout y clearTimeout
setTimeout(funcion, ms, ...argumentos) ejecuta la función una vez pasados al menos ms milisegundos. Devuelve un identificador que sirve para cancelarlo con clearTimeout.
const saludar = (nombre) => console.log(`Hola, ${nombre}`);
const id = setTimeout(saludar, 1000, 'Ana'); // los argumentos extra se pasan a la función
clearTimeout(id); // si cambiamos de idea antes de que pase el segundo, no se muestra nada
setInterval y clearInterval
setInterval repite la función cada ms milisegundos hasta que lo detienes:
let segundos = 3;
const intervalo = setInterval(() => {
console.log(segundos--);
if (segundos === 0) {
clearInterval(intervalo);
console.log('¡Despegue!');
}
}, 1000);
// Salida (una línea por segundo): 3, 2, 1, ¡Despegue!
setInterval lanza la función cada X ms aunque la anterior no haya terminado (por ejemplo, si cada ejecución hace una petición lenta). Un setTimeout que se reprograma al terminar garantiza una pausa entre ejecuciones:
async function sondear() {
try {
await comprobarNovedades(); // una función asíncrona cualquiera
} finally {
setTimeout(sondear, 5000); // la siguiente consulta se programa al acabar esta
}
}
💡 Consejo: Los navegadores ralentizan los temporizadores de las pestañas en segundo plano (pueden pasar a ejecutarse como mucho una vez por segundo, o menos). No uses
setIntervalpara medir el tiempo: guarda la hora de inicio conDate.now()operformance.now()y calcula la diferencia.
| Función | Ejecuta | Cancelar con |
|---|---|---|
setTimeout(fn, ms) | Una vez, tras ms como mínimo | clearTimeout(id) |
setInterval(fn, ms) | Repetidamente cada ms | clearInterval(id) |
queueMicrotask(fn) | Una vez, como microtarea | No se puede cancelar |
4. Callbacks y el callback hell
📘 Concepto: Un callback es una función que pasas a otra para que te llame cuando termine. Es la forma más antigua de gestionar la asincronía en JavaScript: “toma esta función y ejecútala cuando tengas el resultado”.
Ya has usado callbacks síncronos (array.map(x => x * 2)). Los asíncronos son iguales, pero se ejecutan más tarde:
// Simulamos una petición lenta con setTimeout.
// Convención "error primero" (error-first) de Node.js:
// el primer parámetro del callback es el error (o null) y el segundo, el resultado
function obtenerUsuarioCallback(id, callback) {
setTimeout(() => {
if (id <= 0) callback(new Error('Id no válido'));
else callback(null, { id, nombre: 'Ana' });
}, 500);
}
obtenerUsuarioCallback(1, (error, usuario) => {
if (error) return console.error(error.message);
console.log(usuario.nombre); // Ana
});
El problema: el callback hell
Cuando una operación depende del resultado de la anterior, los callbacks se anidan unos dentro de otros y forman la famosa pirámide de la perdición:
obtenerUsuarioCallback(1, (error, usuario) => {
if (error) return mostrarError(error);
obtenerPedidos(usuario.id, (error, pedidos) => {
if (error) return mostrarError(error);
obtenerDetalle(pedidos[0].id, (error, detalle) => {
if (error) return mostrarError(error);
calcularEnvio(detalle, (error, envio) => {
if (error) return mostrarError(error);
console.log('Coste de envío:', envio.coste);
// ...y seguimos hacia la derecha
});
});
});
});
Problemas de este estilo:
- Legibilidad: el código crece hacia la derecha y cuesta seguir el flujo.
- Errores: hay que comprobar el error en cada nivel; si olvidas uno, el fallo se pierde.
- Composición: hacer dos cosas en paralelo y esperar a ambas, o poner un tiempo máximo, es muy laborioso.
- Confianza: dependes de que la otra función llame a tu callback exactamente una vez (ni cero, ni dos).
Las promesas nacieron (ES2015) para resolver todo esto.
5. Promesas
📘 Concepto: Una promesa (
Promise) es un objeto que representa un valor que todavía no tienes pero tendrás en el futuro (o un error si algo sale mal). Es como el ticket con número que te dan en una hamburguesería: no es la hamburguesa, pero te garantiza que cuando esté lista te avisarán, o te dirán que se ha acabado el pan.
Estados de una promesa
stateDiagram-v2
[*] --> Pendiente: new Promise(...)
Pendiente --> Cumplida: resolve(valor)
Pendiente --> Rechazada: reject(error)
Cumplida --> [*]
Rechazada --> [*]
| Estado | Inglés | Significado |
|---|---|---|
| Pendiente | pending | La operación aún no ha terminado |
| Cumplida | fulfilled | Terminó bien y tiene un valor |
| Rechazada | rejected | Falló y tiene un motivo (normalmente un Error) |
Cuando una promesa está cumplida o rechazada se dice que está resuelta o asentada (settled) y ya no puede cambiar de estado.
Crear una promesa con new Promise
El constructor recibe una función ejecutora con dos parámetros: resolve (para cumplirla) y reject (para rechazarla).
function esperar(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function tirarDado() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const valor = Math.ceil(Math.random() * 6);
if (valor === 1) reject(new Error('Ha salido un 1: pierdes'));
else resolve(valor);
}, 500);
});
}
⚠️ Importante: Rechaza siempre con un objeto
Error(o una subclase, como viste en el módulo 10), no con cadenas: así conservas la traza de la pila (stack) y puedes usarinstanceof.
Consumir una promesa: then, catch y finally
tirarDado()
.then((valor) => console.log(`Has sacado un ${valor}`))
.catch((error) => console.error(error.message))
.finally(() => console.log('Partida terminada')); // siempre, haya ido bien o mal
| Método | Se ejecuta cuando… | Recibe |
|---|---|---|
.then(alCumplir) | La promesa se cumple | El valor |
.then(alCumplir, alRechazar) | Cumple o rechaza (segundo argumento, poco usado) | Valor o motivo |
.catch(alRechazar) | La promesa (o cualquier anterior de la cadena) se rechaza | El motivo |
.finally(fn) | Siempre, al asentarse | Nada (no recibe ni valor ni error) |
Encadenamiento
La clave de las promesas: then devuelve una promesa nueva. Lo que devuelvas dentro de un then será el valor del siguiente; si devuelves una promesa, el siguiente then espera a que se resuelva. Así la pirámide se convierte en una lista plana:
obtenerUsuario(1)
.then((usuario) => obtenerPedidos(usuario.id)) // devuelve una promesa
.then((pedidos) => obtenerDetalle(pedidos[0].id))
.then((detalle) => calcularEnvio(detalle))
.then((envio) => console.log('Coste de envío:', envio.coste))
.catch((error) => mostrarError(error)); // un único catch para toda la cadena
Si cualquier paso lanza un error, se saltan los then siguientes y se salta directamente al catch:
Promise.resolve(1)
.then((x) => x + 1) // 2
.then((x) => {
throw new Error(`Fallo en el paso 2 (x = ${x})`);
})
.then(() => 'esto no se ejecuta')
.catch((error) => console.log(error.message));
// Salida: Fallo en el paso 2 (x = 2)
⚠️ Importante: El error más común con promesas es olvidar el
returndentro de unthencon llaves. Si escribes.then((u) => { obtenerPedidos(u.id); }), el siguientethenrecibeundefinedy no espera a nada. Con la arrow function sin llaves (.then((u) => obtenerPedidos(u.id))) elreturnes implícito.
Promise.resolve y Promise.reject
Crean promesas ya asentadas. Son útiles para empezar una cadena, para funciones que a veces tienen el dato en caché, o en los tests:
const cache = new Map();
function obtenerProducto(id) {
if (!Number.isInteger(id)) return Promise.reject(new TypeError('El id debe ser un entero'));
if (cache.has(id)) return Promise.resolve(cache.get(id)); // promesa cumplida al momento
return pedirAlServidor(id).then((producto) => {
cache.set(id, producto);
return producto;
});
}
📘 Concepto: JavaScript trata como promesa a cualquier objeto con un método
then(un thenable). Por esoawaityPromise.resolvefuncionan con librerías antiguas que tenían sus propias promesas.
Promisificar: convertir callbacks en promesas
Muchas APIs antiguas usan callbacks. Envolverlas en una promesa se llama promisificar. Así quedaría obtenerUsuarioCallback de la sección anterior:
function obtenerUsuario(id) {
return new Promise((resolve, reject) => {
obtenerUsuarioCallback(id, (error, usuario) => {
if (error) reject(error);
else resolve(usuario);
});
});
}
obtenerUsuario(1).then((u) => console.log(u.nombre)); // Ana
Node.js trae la utilidad promisify y versiones con promesas de sus módulos:
import { promisify } from 'node:util';
import { setTimeout as esperar } from 'node:timers/promises';
import { readFile } from 'node:fs/promises';
const obtenerUsuarioP = promisify(obtenerUsuarioCallback); // para funciones error-first
await esperar(1000); // setTimeout que devuelve una promesa
const texto = await readFile('notas.txt', 'utf8');
6. Combinar varias promesas
Con frecuencia lanzas varias operaciones a la vez y quieres esperar a todas, a la primera, o a la primera que salga bien. Para eso están los combinadores: reciben un iterable de promesas y devuelven una promesa nueva.
En los ejemplos usaremos estas funciones auxiliares (creamos las promesas al llamar a cada función, no antes, para no dejar rechazos sin gestionar):
const esperar = (ms, valor) => new Promise((resolve) => setTimeout(() => resolve(valor), ms));
const rapida = () => esperar(100, 'rápida');
const lenta = () => esperar(300, 'lenta');
const fallida = () => new Promise((_, reject) => setTimeout(() => reject(new Error('fallo')), 200));
Promise.all: todas o ninguna
Espera a que todas se cumplan y devuelve un array con los valores en el mismo orden en que las pasaste (no en el orden en que terminan). Si una falla, rechaza inmediatamente con ese error.
console.log(await Promise.all([rapida(), lenta()])); // [ 'rápida', 'lenta' ] (~300 ms, no 400)
await Promise.all([rapida(), fallida(), lenta()]).catch((e) => console.log(e.message)); // fallo (~200 ms)
Promise.allSettled: espera a todas, salgan como salgan
Nunca rechaza. Devuelve un array de objetos { status, value } o { status, reason }:
const resultados = await Promise.allSettled([rapida(), fallida()]);
console.log(resultados);
// [
// { status: 'fulfilled', value: 'rápida' },
// { status: 'rejected', reason: Error: fallo ... }
// ]
const correctos = resultados.filter((r) => r.status === 'fulfilled').map((r) => r.value);
Promise.any: la primera que salga bien
Se cumple con la primera promesa cumplida e ignora los fallos. Solo rechaza si todas fallan, con un AggregateError que contiene todos los errores en errors:
console.log(await Promise.any([fallida(), lenta()])); // lenta
try {
await Promise.any([Promise.reject(new Error('a')), Promise.reject(new Error('b'))]);
} catch (error) {
console.log(error.name, '-', error.message); // AggregateError - All promises were rejected
console.log(error.errors.map((e) => e.message)); // [ 'a', 'b' ]
}
Promise.race: la primera en terminar
Se asienta con la primera que termine, bien o mal:
console.log(await Promise.race([rapida(), lenta()])); // rápida
await Promise.race([fallida(), lenta()]).catch((e) => console.log(e.message)); // fallo (termina antes)
Tabla comparativa
| Combinador | Se cumple cuando… | Rechaza cuando… | Resultado | Uso típico |
|---|---|---|---|---|
Promise.all | Todas se cumplen | Una falla (la primera) | Array de valores | Datos que necesitas todos (perfil + permisos) |
Promise.allSettled | Todas terminan | Nunca | Array de { status, value/reason } | Lotes en los que algunos pueden fallar |
Promise.any | Una se cumple | Todas fallan (AggregateError) | El primer valor | Varios servidores espejo: el primero que responda |
Promise.race | La primera termina bien | La primera termina mal | El primer valor o error | Competir con un temporizador |
⚠️ Importante: Los combinadores no cancelan las promesas que “pierden”. En
Promise.race, la petición lenta sigue en marcha aunque ya no te interese su resultado. Para cancelarla de verdad necesitasAbortController(sección 11).
Promise.withResolvers() (ES2024)
A veces necesitas crear una promesa y resolverla desde fuera del constructor (por ejemplo, desde un manejador de eventos). Antes había que sacar resolve y reject a variables externas; ahora:
const { promise, resolve, reject } = Promise.withResolvers();
setTimeout(() => resolve('resuelta desde fuera'), 50);
console.log(await promise); // resuelta desde fuera
// Un caso real: una promesa que se cumple cuando el usuario pulsa un botón
function esperarClic(boton) {
const { promise, resolve } = Promise.withResolvers();
boton.addEventListener('click', resolve, { once: true });
return promise;
}
await esperarClic(document.querySelector('#aceptar'));
console.log('El usuario ha aceptado');
Promise.try() (ES2025)
Promise.try(fn, ...args) ejecuta fn y siempre devuelve una promesa: si fn devuelve un valor, la promesa se cumple con él; si devuelve una promesa, la sigue; y si lanza un error síncrono, la promesa se rechaza (en vez de reventar fuera de la cadena).
function leerConfiguracion(texto) {
return JSON.parse(texto); // puede lanzar SyntaxError de forma síncrona
}
Promise.try(leerConfiguracion, '{ mal json')
.then((config) => console.log(config))
.catch((error) => console.log('Capturado:', error.name)); // Capturado: SyntaxError
console.log(await Promise.try((a, b) => a + b, 2, 3)); // 5
💡 Consejo:
Promise.tryes útil cuando recibes una función que no sabes si es síncrona o asíncrona (por ejemplo, un plugin o un callback del usuario) y quieres tratar todos sus errores en el mismocatch.
7. async y await
Las promesas eliminaron la pirámide, pero las cadenas de then siguen siendo distintas del código “normal”. async/await (ES2017) permite escribir código asíncrono con el aspecto de código síncrono.
📘 Concepto:
- Una función marcada con
asyncdevuelve siempre una promesa. Si hacesreturn valor, la promesa se cumple convalor; si lanzas un error, se rechaza.- Dentro de ella,
await promesapausa esa función (no el programa entero) hasta que la promesa se asienta, y devuelve su valor o lanza su error.
async function sumar(a, b) {
return a + b;
}
console.log(sumar(2, 3)); // Promise { 5 }
console.log(await sumar(2, 3)); // 5
Compara la cadena de then de la sección anterior con su versión async:
async function mostrarCosteEnvio(idUsuario) {
const usuario = await obtenerUsuario(idUsuario);
const pedidos = await obtenerPedidos(usuario.id);
const detalle = await obtenerDetalle(pedidos[0].id);
const envio = await calcularEnvio(detalle);
console.log('Coste de envío:', envio.coste);
}
Se lee de arriba abajo, como una receta. async vale para cualquier tipo de función: declaraciones, expresiones, arrow functions (async () => {}) y métodos de clases u objetos (async cargar() {}).
Gestionar errores con try/catch
Un await sobre una promesa rechazada lanza el error, así que se captura con el try/catch de toda la vida (módulo 10):
async function jugar() {
try {
const valor = await tirarDado();
console.log(`Has sacado un ${valor}`);
} catch (error) {
console.error('Error:', error.message);
} finally {
console.log('Partida terminada');
}
}
También puedes combinar estilos: const prefs = await cargarPreferencias().catch(() => ({ tema: 'claro' })); da un valor por defecto si falla, sin try/catch. Y si haces await de algo que no es una promesa, obtienes ese valor tal cual (await 42 vale 42).
⚠️ Importante:
return await promesayreturn promesase comportan distinto dentro de untry. Conreturn promesa, si la promesa falla, el error no pasa por tucatch(la función ya ha devuelto la promesa). Conreturn await promesa, sí. Si quieres capturar el error en esa función, usareturn await.
8. Secuencial frente a paralelo
Este es el error de rendimiento más habitual con async/await: encadenar await de operaciones independientes.
const esperar = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// ❌ Secuencial: cada espera empieza cuando acaba la anterior
console.time('secuencial');
for (const ms of [200, 200, 200]) await esperar(ms);
console.timeEnd('secuencial'); // secuencial: ~600 ms
// ✅ Paralelo: lanzamos las tres y esperamos a todas juntas
console.time('paralelo');
await Promise.all([200, 200, 200].map((ms) => esperar(ms)));
console.timeEnd('paralelo'); // paralelo: ~200 ms
Regla práctica:
- Si la operación B necesita el resultado de A → secuencial (
awaituno tras otro). - Si son independientes → lánzalas a la vez y espera con
Promise.all(oallSettled).
// Perfil y notificaciones no dependen entre sí: en paralelo
const [perfil, notificaciones] = await Promise.all([obtenerPerfil(id), obtenerNotificaciones(id)]);
Errores comunes de await en bucles
1. forEach no espera. forEach ignora la promesa que devuelve tu callback async:
const resultado = [];
[3, 1, 2].forEach(async (n) => {
await esperar(n * 10);
resultado.push(n);
});
console.log(resultado); // [] ← ¡todavía no ha terminado nada!
// (unos milisegundos después resultado valdrá [1, 2, 3], en orden de llegada)
Soluciones: for...of con await si necesitas orden secuencial, o map + Promise.all si puede ir en paralelo.
for (const n of [3, 1, 2]) await procesar(n); // secuencial y en orden
const procesados = await Promise.all([3, 1, 2].map((n) => procesar(n))); // paralelo
2. map con async devuelve promesas, no valores.
const usuarios = [1, 2, 3].map(async (id) => obtenerUsuario(id));
console.log(usuarios); // [ Promise { <pending> }, Promise { <pending> }, Promise { <pending> } ]
const reales = await Promise.all(usuarios); // ✅ ahora sí
3. filter y reduce con async no funcionan como esperas. Una promesa siempre es “verdadera”, así que filter(async ...) no filtra nada. Resuelve primero los datos con Promise.all y filtra después.
Paralelo con límite de concurrencia
Lanzar 1000 peticiones a la vez puede saturar el servidor. Un patrón sencillo es procesar por lotes (lotes en secuencia, elementos de cada lote en paralelo):
async function enLotes(elementos, tamanoLote, tarea) {
const resultados = [];
for (let i = 0; i < elementos.length; i += tamanoLote) {
resultados.push(...(await Promise.all(elementos.slice(i, i + tamanoLote).map(tarea))));
}
return resultados;
}
const imagenes = await enLotes(urls, 5, (url) => descargar(url)); // de 5 en 5
9. Top-level await
En los módulos ES (ficheros .mjs, proyectos con "type": "module" o <script type="module"> en el navegador) puedes usar await fuera de cualquier función, en el nivel superior del fichero (ES2022):
// configuracion.mjs
import { readFile } from 'node:fs/promises';
const texto = await readFile(new URL('./config.json', import.meta.url), 'utf8');
export const configuracion = JSON.parse(texto);
Los módulos que importen configuracion.mjs esperarán a que termine su await antes de ejecutarse, así que siempre recibirán la configuración ya cargada.
⚠️ Importante: El top-level await solo funciona en módulos. En un script clásico (
<script>sintype="module"o un.cjsde Node) obtendrás unSyntaxError. Úsalo con moderación: unawaitlento en un módulo retrasa a todos los que lo importan. Verás los módulos a fondo en el módulo 13.
10. Iteración asíncrona
Generadores asíncronos y for await...of
En el módulo 11 viste los generadores (function*), que producen valores bajo demanda. Un generador asíncrono (async function*) puede hacer await entre valor y valor, y se recorre con for await...of. Es perfecto para datos que llegan por partes: páginas de una API, líneas de un fichero, mensajes de un stream.
// Simula una API que devuelve los resultados página a página
async function* paginas(total) {
for (let pagina = 1; pagina <= total; pagina++) {
await new Promise((r) => setTimeout(r, 20)); // simulamos la red
yield [`item-${pagina}-a`, `item-${pagina}-b`];
}
}
for await (const lote of paginas(3)) {
console.log('Lote:', lote.join(', '));
}
// Salida: Lote: item-1-a, item-1-b / Lote: item-2-a, item-2-b / Lote: item-3-a, item-3-b
Con una API real quedaría así. Usamos JSONPlaceholder (jsonplaceholder.typicode.com), una API pública y gratuita de datos falsos pensada para practicar, que acepta los parámetros _page y _limit para paginar (verás fetch a fondo en el módulo 17):
async function* todosLosPosts() {
for (let pagina = 1; ; pagina++) {
const respuesta = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${pagina}&_limit=20`);
const posts = await respuesta.json();
if (posts.length === 0) return; // no hay más páginas
yield* posts; // entrega los posts de uno en uno
}
}
for await (const post of todosLosPosts()) {
if (post.id > 45) break; // break cierra el generador: no se piden más páginas
console.log(post.id, post.title);
}
for await...of también funciona con un array de promesas: espera a cada una en orden.
Array.fromAsync() (ES2026)
Array.fromAsync es el hermano asíncrono de Array.from: recorre un iterable asíncrono (o un iterable de promesas) y devuelve una promesa con un array de todos los valores.
async function* contar(hasta) {
for (let i = 1; i <= hasta; i++) {
await new Promise((r) => setTimeout(r, 10));
yield i;
}
}
console.log(await Array.fromAsync(contar(3))); // [ 1, 2, 3 ]
console.log(await Array.fromAsync(contar(3), (n) => n * 10)); // [ 10, 20, 30 ] (con mapeo)
console.log(await Array.fromAsync([Promise.resolve('a'), 'b'])); // [ 'a', 'b' ] (iterable de promesas)
// Juntar todas las páginas del generador anterior en un solo array
console.log((await Array.fromAsync(paginas(2))).flat()); // [ 'item-1-a', 'item-1-b', 'item-2-a', 'item-2-b' ]
⚠️ Importante:
Array.fromAsyncespera los elementos de uno en uno, en secuencia, igual quefor await...of. Para un array de promesas ya lanzadas que quieres esperar en paralelo, sigue prefiriendoPromise.all: rechaza en cuanto cualquiera falla, mientras queArray.fromAsyncno “ve” un fallo hasta que llega a esa posición. Y no lo uses con iterables infinitos: nunca terminaría.
11. Cancelar operaciones: AbortController y AbortSignal
Las promesas no se pueden cancelar por sí mismas. El mecanismo estándar (navegador y Node.js) es el AbortController:
📘 Concepto: Un
AbortControlleres el mando a distancia y susignal(unAbortSignal) es el receptor que entregas a la operación. Cuando pulsas el botón (controlador.abort()), la señal se marca como abortada (signal.aborted === true), guarda el motivo ensignal.reasony emite el eventoabort.
const controlador = new AbortController();
fetch('https://jsonplaceholder.typicode.com/photos', { signal: controlador.signal })
.then((respuesta) => respuesta.json())
.then((fotos) => console.log(fotos.length))
.catch((error) => {
if (error.name === 'AbortError') console.log('Petición cancelada por el usuario');
else throw error;
});
controlador.abort(); // por ejemplo, al pulsar "Cancelar"
Tiempo máximo con AbortSignal.timeout(ms)
Crea una señal que se aborta sola pasado un tiempo, con un error de nombre TimeoutError:
try {
const respuesta = await fetch('https://jsonplaceholder.typicode.com/users', { signal: AbortSignal.timeout(5000) });
console.log(await respuesta.json());
} catch (error) {
if (error.name === 'TimeoutError') console.log('El servidor ha tardado más de 5 s');
}
Combinar señales con AbortSignal.any([...])
Devuelve una señal que se aborta en cuanto cualquiera de las señales que le pasas se aborte. Ideal para “cancelar si el usuario lo pide o si pasan 10 segundos”:
const usuario = new AbortController();
const signal = AbortSignal.any([usuario.signal, AbortSignal.timeout(10_000)]);
botonCancelar.addEventListener('click', () => usuario.abort(new Error('El usuario canceló')));
await fetch(url, { signal });
Hacer cancelables tus propias funciones
Cualquier función asíncrona puede aceptar una signal. El patrón es: comprobar si ya está abortada, escuchar el evento abort y limpiar lo que tengas en marcha.
function esperar(ms, { signal } = {}) {
return new Promise((resolve, reject) => {
signal?.throwIfAborted(); // si ya estaba abortada, lanza signal.reason
const id = setTimeout(resolve, ms);
signal?.addEventListener(
'abort',
() => {
clearTimeout(id); // limpiamos el temporizador
reject(signal.reason); // rechazamos con el motivo de la cancelación
},
{ once: true },
);
});
}
const controlador = new AbortController();
setTimeout(() => controlador.abort(), 50);
await esperar(500, { signal: controlador.signal }).catch((e) => console.log(e.name, '-', e.message));
// AbortError - This operation was aborted
await esperar(500, { signal: AbortSignal.timeout(60) }).catch((e) => console.log(e.name, '-', e.message));
// TimeoutError - The operation was aborted due to timeout
| API | Para qué sirve |
|---|---|
new AbortController() | Crea el mando; su propiedad signal es la señal |
controlador.abort(motivo?) | Aborta la señal (motivo por defecto: DOMException llamada AbortError) |
signal.aborted | true si ya se ha abortado |
signal.reason | El motivo de la cancelación |
signal.throwIfAborted() | Lanza signal.reason si está abortada |
AbortSignal.timeout(ms) | Señal que se aborta sola con un TimeoutError |
AbortSignal.any(señales) | Señal que se aborta cuando lo hace cualquiera de las dadas |
AbortSignal.abort(motivo?) | Señal ya abortada desde el principio (útil en tests) |
💡 Consejo: La misma
signalsirve para quitar listeners de eventos (addEventListener(tipo, fn, { signal })), como verás en el módulo 15. Un soloabort()puede cancelar peticiones, temporizadores y listeners a la vez.
12. Errores no capturados
Si una promesa se rechaza y nadie la gestiona (sin catch ni try/catch alrededor del await), el error no desaparece:
- En el navegador aparece en la consola como
Uncaught (in promise)y se emite el eventounhandledrejectionenwindow. - En Node.js (desde la versión 15), un rechazo no gestionado termina el proceso con código de error, salvo que escuches el evento
unhandledRejectiondeprocess.
// Navegador: red de seguridad global para registrar errores
window.addEventListener('unhandledrejection', (evento) => {
console.error('Promesa rechazada sin gestionar:', evento.reason);
enviarAlServicioDeLogs(evento.reason); // por ejemplo
// evento.preventDefault(); evitaría el mensaje por defecto en consola
});
// Node.js
process.on('unhandledRejection', (motivo) => {
console.error('Rechazo no gestionado:', motivo.message);
process.exitCode = 1;
});
setTimeout(() => Promise.reject(new Error('nadie me escucha')), 100);
// Salida: Rechazo no gestionado: nadie me escucha
Una trampa sutil
Una promesa que se rechaza antes de que le pongas el catch cuenta como no gestionada:
const fallo = new Promise((_, reject) => setTimeout(() => reject(new Error('fallo')), 200));
const lenta = new Promise((resolve) => setTimeout(resolve, 300));
await lenta; // mientras esperamos, "fallo" se rechaza sin manejador...
await fallo.catch(() => {}); // ...y llegamos tarde: en Node el proceso ya ha terminado
La solución es esperar a ambas juntas (await Promise.all([lenta, fallo])) o crear cada promesa justo cuando vayas a esperarla.
⚠️ Importante: Los manejadores globales son una red de seguridad para registrar errores, no una forma de gestionarlos. Cada promesa debería tener su
catcho sutry/catchen el lugar adecuado. Algunas reglas de linting con información de tipos (comono-floating-promisesdel paquete typescript-eslint) detectan promesas “flotantes” sin gestionar; ESLint y TypeScript los verás en el módulo 21.
13. Ejemplo completo: descargas simuladas con reintentos
Vamos a juntar casi todo lo del módulo en un programa de Node.js que simula tres descargas en paralelo, cada una con su duración y con fallos de red aleatorios (aquí, fijos para que la salida sea reproducible). Las descargas fallidas se reintentan con backoff exponencial (cada espera es el doble que la anterior) y todo el proceso tiene un tiempo máximo global.
📘 Concepto: El backoff exponencial consiste en esperar cada vez más entre reintentos (100 ms, 200 ms, 400 ms…). Si un servidor está saturado, reintentar inmediatamente solo empeora las cosas; esperar más cada vez le da tiempo a recuperarse.
// descargas.mjs — simulador de descargas con reintentos y backoff exponencial
const inicio = performance.now();
const t = () => `[${String(Math.round((performance.now() - inicio) / 100) * 100).padStart(4)} ms]`;
// La función esperar cancelable de la sección 11
function esperar(ms, { signal } = {}) {
return new Promise((resolve, reject) => {
signal?.throwIfAborted();
const id = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => { clearTimeout(id); reject(signal.reason); }, { once: true });
});
}
class ErrorDescarga extends Error {
constructor(mensaje, { archivo, intento, ...opciones } = {}) {
super(mensaje, opciones);
this.name = 'ErrorDescarga';
this.archivo = archivo;
this.intento = intento;
}
}
// Catálogo simulado: cuánto tarda cada archivo y cuántas veces falla antes de funcionar
const catalogo = [
{ nombre: 'foto.jpg', ms: 300, fallos: 0 },
{ nombre: 'video.mp4', ms: 500, fallos: 2 },
{ nombre: 'informe.pdf', ms: 200, fallos: 5 },
];
const intentosRealizados = new Map();
// Simula una descarga: falla las primeras "fallos" veces
async function descargar(archivo, { signal } = {}) {
const intento = (intentosRealizados.get(archivo.nombre) ?? 0) + 1;
intentosRealizados.set(archivo.nombre, intento);
await esperar(archivo.ms, { signal });
if (intento <= archivo.fallos) {
throw new ErrorDescarga(`Fallo de red al descargar ${archivo.nombre}`, { archivo: archivo.nombre, intento });
}
return { nombre: archivo.nombre, bytes: archivo.ms * 1024, intentos: intento };
}
// Ejecuta "tarea" y, si falla, la reintenta esperando cada vez el doble
async function conReintentos(tarea, { reintentos = 3, esperaBase = 100, signal } = {}) {
for (let intento = 0; ; intento++) {
try {
return await tarea();
} catch (error) {
// No reintentamos si nos han cancelado o si ya no quedan intentos
if (signal?.aborted || intento >= reintentos) throw error;
const espera = esperaBase * 2 ** intento; // 100, 200, 400, 800...
console.log(`${t()} ⚠ ${error.message}. Reintento en ${espera} ms`);
await esperar(espera, { signal });
}
}
}
async function descargarTodo(archivos, { tiempoMaximo = 5000 } = {}) {
const signal = AbortSignal.timeout(tiempoMaximo);
const resultados = await Promise.allSettled(
archivos.map(async (archivo) => {
const datos = await conReintentos(() => descargar(archivo, { signal }), { signal });
console.log(`${t()} ✔ ${datos.nombre} (${datos.intentos} intento/s)`);
return datos;
}),
);
const correctos = resultados.filter((r) => r.status === 'fulfilled').map((r) => r.value);
const fallidos = resultados.filter((r) => r.status === 'rejected').map((r) => r.reason);
return { correctos, fallidos };
}
console.log(`${t()} Empiezan ${catalogo.length} descargas en paralelo`);
const { correctos, fallidos } = await descargarTodo(catalogo);
console.log(`${t()} Terminado: ${correctos.length} correctas, ${fallidos.length} fallidas`);
const totalKb = correctos.reduce((suma, d) => suma + d.bytes, 0) / 1024;
console.log(`Descargados ${totalKb} KB`);
for (const error of fallidos) {
console.log(`✘ ${error.archivo}: ${error.message} (último intento: ${error.intento})`);
}
Salida real con node descargas.mjs (los tiempos pueden variar en ±100 ms):
[ 0 ms] Empiezan 3 descargas en paralelo
[ 200 ms] ⚠ Fallo de red al descargar informe.pdf. Reintento en 100 ms
[ 300 ms] ✔ foto.jpg (1 intento/s)
[ 500 ms] ⚠ Fallo de red al descargar video.mp4. Reintento en 100 ms
[ 500 ms] ⚠ Fallo de red al descargar informe.pdf. Reintento en 200 ms
[ 900 ms] ⚠ Fallo de red al descargar informe.pdf. Reintento en 400 ms
[1100 ms] ⚠ Fallo de red al descargar video.mp4. Reintento en 200 ms
[1800 ms] ✔ video.mp4 (3 intento/s)
[1800 ms] Terminado: 2 correctas, 1 fallidas
Descargados 800 KB
✘ informe.pdf: Fallo de red al descargar informe.pdf (último intento: 4)
Repasa qué concepto aparece en cada pieza: esperar es un setTimeout promisificado y cancelable; descargar usa async/await y rechaza con un error personalizado; conReintentos combina try/catch, return await y backoff; map(async ...) + Promise.allSettled ejecuta en paralelo tolerando fallos; AbortSignal.timeout pone un tiempo máximo global, y el await descargarTodo(...) final es top-level await.
✏️ Ejercicio: Cambia
tiempoMaximoa1000y vuelve a ejecutarlo. ¿Qué descargas terminan? ¿Qué nombre de error tienen las demás? Después añade aconReintentosun “jitter” (un pequeño retardo aleatorio sumado a la espera) para que muchos clientes no reintenten exactamente a la vez.
14. Ejercicios
Ejercicio 1: Predice el orden
Sin ejecutarlo, escribe en un papel el orden de salida del siguiente programa. Luego compruébalo con Node y explica cada línea según las reglas del event loop.
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C')).then(() => console.log('D'));
queueMicrotask(() => console.log('E'));
setTimeout(() => Promise.resolve().then(() => console.log('F')), 0);
console.log('G');
Ejercicio 2: Cuenta atrás
Crea una función cuentaAtras(segundos) que devuelva una promesa. Debe mostrar por consola cada segundo restante con setInterval y cumplirse cuando llegue a 0 (limpiando el intervalo). Úsala con await cuentaAtras(5); console.log('¡Tiempo!');.
Ejercicio 3: Promisificar
Dada la función con callback leerTemperatura(ciudad, callback) que, tras 300 ms, llama a callback(null, grados) o a callback(new Error('Ciudad desconocida')) si la ciudad no está en un objeto { Madrid: 31, Oslo: 12 }, escribe la versión leerTemperaturaAsync(ciudad) que devuelva una promesa. Hazlo primero a mano y después con promisify de node:util.
Ejercicio 4: Paralelo frente a secuencial
Con leerTemperaturaAsync del ejercicio anterior, obtén la temperatura de cinco ciudades: primero en secuencia con for...of y después en paralelo con Promise.all. Mide ambos tiempos con console.time. Luego usa Promise.allSettled para que una ciudad desconocida no impida mostrar las demás.
Ejercicio 5: Tiempo máximo con race
Implementa conTiempoMaximo(promesa, ms) que devuelva una promesa que se rechaza con un Error('Tiempo agotado') si promesa no termina en ms milisegundos. Usa Promise.race. Después reescríbela para que reciba una función que acepte una signal y use AbortSignal.timeout(ms), de modo que la operación lenta se cancele de verdad.
Ejercicio 6: Semáforo con generador asíncrono
Escribe un generador asíncrono semaforo() que emita indefinidamente 'verde', 'ámbar' y 'rojo', esperando 3 s, 1 s y 2 s respectivamente entre cada cambio. Recórrelo con for await...of y detente (break) tras dos ciclos completos. Después usa Array.fromAsync con un generador finito (un solo ciclo) para obtener un array con los tres colores.
Ejercicio 7: Cola de tareas cancelable
Crea una función procesarCola(tareas, { signal }) que reciba un array de funciones asíncronas y las ejecute de una en una, devolviendo un array con sus resultados. Si la señal se aborta, debe dejar de procesar en cuanto termine la tarea actual y rechazar con signal.reason. Pruébala con un AbortController que se aborte a los 2 segundos.
Ejercicio 8: Reintentos genéricos
Mejora conReintentos del ejemplo completo para que acepte una opción debeReintentar(error) (por defecto, reintentar siempre) y un maximoEspera que limite el backoff. Por ejemplo, que no reintente si el error es un TypeError y que nunca espere más de 2 segundos entre intentos.
Resumen
| Concepto | Sintaxis / clave |
|---|---|
| Asincronía | Lanzar una operación lenta y seguir; un solo hilo que no se bloquea |
| Event loop | Código síncrono → todas las microtareas → repintado → una macrotarea |
| Macrotareas | setTimeout, setInterval, eventos, mensajes |
| Microtareas | then/catch/finally, continuación tras await, queueMicrotask |
| Temporizadores | setTimeout(fn, ms) / clearTimeout(id), setInterval / clearInterval |
| Callback | Función que se ejecuta al terminar; convención error-first (error, dato) |
| Promesa | Estados pendiente → cumplida o rechazada; new Promise((resolve, reject) => ...) |
| Consumir | .then(valor => ...), .catch(error => ...), .finally(() => ...) |
| Promesas ya asentadas | Promise.resolve(valor), Promise.reject(error) |
| Promisificar | new Promise envolviendo el callback, promisify de node:util |
| Combinadores | all (todas o ninguna), allSettled (todas con su estado), any (la primera que funcione; AggregateError si todas fallan), race (la primera en terminar) |
| Resolver desde fuera | const { promise, resolve, reject } = Promise.withResolvers() (ES2024) |
| Capturar errores síncronos | Promise.try(fn, ...args) (ES2025) |
async/await | async function f() { const x = await promesa; } |
Errores con await | try { await ... } catch (e) { ... } finally { ... } |
| Paralelo | await Promise.all(lista.map(fn)); nunca forEach con async |
| Top-level await | await en el nivel superior de un módulo ES |
| Generador asíncrono | async function* gen() { yield await ... } + for await (const x of gen()) |
| Recoger en array | await Array.fromAsync(iterableAsincrono, mapeo?) (ES2026) |
| Cancelar | new AbortController(), controlador.abort(), { signal } |
| Tiempo máximo | AbortSignal.timeout(ms), combinar con AbortSignal.any([...]) |
| Rechazos sin gestionar | unhandledrejection (navegador), process.on('unhandledRejection') (Node) |