Capítulo 12 — JavaScript asíncrono¶
Introducción¶
JavaScript es monohilo: solo puede ejecutar una cosa a la vez. Pero la web es inherentemente asíncrona: descargas, temporizadores, eventos, peticiones a APIs… Todas esas cosas tardan y no pueden bloquear la interfaz.
Este capítulo explica cómo JavaScript maneja la asincronía y cómo escribir código legible, seguro y moderno.
El problema: código bloqueante¶
console.log('Inicio');
const datos = descargaBloqueante(); // bloquea 3 segundos
console.log(datos);
console.log('Fin');
Mientras se ejecuta descargaBloqueante, el navegador se congela. Nadie puede hacer clic, ni scroll, ni nada.
Callbacks (el enfoque antiguo)¶
La primera solución fueron los callbacks: funciones que se ejecutan cuando termina la operación.
descargar('https://api.com/datos', (error, resultado) => {
if (error) {
console.error('Falló:', error);
return;
}
console.log('Datos:', resultado);
});
El infierno de los callbacks¶
login(usuario, (err, sesion) => {
obtenerPerfil(sesion.id, (err, perfil) => {
obtenerPosts(perfil.id, (err, posts) => {
obtenerComentarios(posts[0].id, (err, comentarios) => {
// 😱
});
});
});
});
Anidar callbacks genera código difícil de leer y de mantener. La comunidad lo llamó callback hell.
Promesas¶
Una Promise es un objeto que representa un valor que estará disponible ahora, en el futuro o nunca.
Tiene tres estados:
- pending — todavía no se ha resuelto.
- fulfilled — se resolvió con un valor.
- rejected — falló con un error.
Crear una promesa¶
const esperar = (ms) => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
esperar(1000).then(() => console.log('Pasó 1 segundo'));
Encadenar promesas¶
fetch('https://api.com/usuarios')
.then(res => res.json())
.then(usuarios => {
console.log(usuarios);
return fetch(`https://api.com/posts/${usuarios[0].id}`);
})
.then(res => res.json())
.then(posts => console.log(posts))
.catch(err => console.error('Error:', err));
Mucho mejor que los callbacks anidados, pero aún hay que leer .then y .catch uno tras otro.
async / await¶
La sintaxis moderna. Convierte código asíncrono en algo que se lee como código síncrono.
async function obtenerDatos() {
try {
const res = await fetch('https://api.com/usuarios');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const usuarios = await res.json();
const resPosts = await fetch(`https://api.com/posts/${usuarios[0].id}`);
const posts = await resPosts.json();
return posts;
} catch (err) {
console.error('Error:', err.message);
}
}
Reglas:
asyncconvierte una función en una promesa.awaitpausa la ejecución de la función hasta que la promesa se resuelva.awaitsolo puede usarse dentro de funcionesasync(o en el top-level de un módulo).
Consejo
Envuelve siempre las operaciones await en try/catch. Sin eso, cualquier fallo en la red propagará el error.
Promise.all, Promise.allSettled y Promise.race¶
Promise.all — esperar a que todas terminen¶
const [usuarios, posts, comentarios] = await Promise.all([
fetch('/api/usuarios').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comentarios').then(r => r.json()),
]);
Si una falla, todo el Promise.all falla. Útil cuando todas son necesarias.
Promise.allSettled — esperar sin importar si fallan¶
const resultados = await Promise.allSettled([
fetch('/api/a'),
fetch('/api/b'),
fetch('/api/c'),
]);
resultados.forEach(r => {
if (r.status === 'fulfilled') console.log('OK:', r.value);
else console.error('Fallo:', r.reason);
});
Promise.race — la primera que responda gana¶
const resultado = await Promise.race([
fetch('/api/rapida'),
esperar(3000).then(() => { throw new Error('Timeout'); }),
]);
Perfecto para implementar timeouts.
Cancelar peticiones con AbortController¶
¿Qué pasa si el usuario cambia de página antes de que llegue la respuesta? Hay que cancelar la petición.
const controller = new AbortController();
fetch('/api/datos', { signal: controller.signal })
.then(res => res.json())
.catch(err => {
if (err.name === 'AbortError') console.log('Cancelado');
else console.error('Error real:', err);
});
// En algún momento:
controller.abort();
Con async/await:
async function buscar(texto) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch(`/api/buscar?q=${texto}`, {
signal: controller.signal,
});
return await res.json();
} catch (err) {
if (err.name === 'AbortError') return null;
throw err;
} finally {
clearTimeout(timeout);
}
}
Async iterators y for await...of¶
async function* contar() {
for (let i = 1; i <= 3; i++) {
await esperar(1000);
yield i;
}
}
for await (const n of contar()) {
console.log(n);
}
Útil para streams de datos.
Patrones reales¶
Debounce de búsqueda¶
function debounce(fn, ms) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), ms);
};
}
const buscar = debounce(async (texto) => {
const res = await fetch(`/api/buscar?q=${texto}`);
const datos = await res.json();
renderizar(datos);
}, 300);
input.addEventListener('input', e => buscar(e.target.value));
Reintentos con backoff exponencial¶
async function fetchConReintentos(url, intentos = 3) {
for (let i = 0; i < intentos; i++) {
try {
const res = await fetch(url);
if (res.ok) return await res.json();
} catch (err) {
if (i === intentos - 1) throw err;
}
await esperar(2 ** i * 1000);
}
}
Caché de promesas¶
const cache = new Map();
function fetchCache(url) {
if (!cache.has(url)) {
cache.set(url, fetch(url).then(r => r.json()));
}
return cache.get(url);
}
// Llamadas repetidas solo disparan una petición
await Promise.all([fetchCache('/api/a'), fetchCache('/api/a')]);
Buenas prácticas¶
- Usa siempre
async/awaiten código nuevo. Los callbacks anidados son un antipatrón. - Envuelve en
try/catchcada bloqueawaitque pueda fallar. - Comprueba
res.okantes de llamar ares.json()en unfetch. - Cancela peticiones con
AbortControllercuando el usuario navega o cambia de vista. - No mezcles
thenyawaiten la misma función sin motivo. - Usa
Promise.allcuando varias operaciones pueden ir en paralelo. - Nunca bloquees el hilo con bucles largos o cálculos pesados; usa Web Workers.
Ejercicios¶
- Reescribe un callback anidado de tres niveles usando
async/await. - Implementa una función
buscarConTimeout(url, ms)que aborte la petición si excede el tiempo. - Simula un sistema de descargas paralelas con
Promise.ally muestra el progreso. - Crea un buscador con
debounceque consulte una API al escribir. - Implementa un patrón de reintento con 3 intentos y espera exponencial.
⬅️ Capítulo 11 — CSS moderno | ➡️ Capítulo 13 — Módulos y bundlers (pendiente)tail -5 docs/capitulos/12-javascript-asincrono.md