Promesas y async/await: trucos y buenas prácticas para código asíncrono

En el día a día de un frontend, la asincronía es inevitable: peticiones a APIs, temporizadores, websockets… Todo eso se maneja con promesas y la sintaxis async/await. En este artículo vamos más allá de la teoría básica y nos centramos en patrones que facilitan el mantenimiento, la depuración y el rendimiento.
1. Repaso rápido: cómo se crean y encadenan promesas
Una promesa representa un valor que llegará en el futuro. Se crea con new Promise y se resuelve o rechaza mediante resolve y reject. El método then permite encadenar transformaciones y catch captura errores.
const delay = ms => new Promise((resolve) => setTimeout(resolve, ms));
delay(1000).then(() => console.log('un segundo después'));
2. async/await en la práctica
La sintaxis async/await es azúcar sintáctico que hace que el código asíncrono se lea como síncrono. Un detalle importante: await solo funciona dentro de una función marcada como async.
async function fetchJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(Error ${response.status});
}
return await response.json();
}
Si la promesa rechaza, el error se propaga como una excepción que podemos capturar con try…catch.
3. Manejo de errores consistente
Un error típico es mezclar .then/.catch con async/await, lo que genera bloques de captura confusos. Lo recomendable es escoger un estilo por función y usar try…catch para todo lo que esté dentro de async. Además, finally permite ejecutar código sin importar el resultado.
async function loadData() {
try {
const user = await fetchJson('/api/user');
const posts = await fetchJson(/api/posts?uid=${user.id});
return {user, posts};
} catch (err) {
console.error('Fallo al cargar datos:', err);
throw err; // re‑propagar para que el llamador decida
} finally {
console.log('Operación de carga finalizada');
}
}
4. Paralelismo controlado con Promise.all y Promise.race
Cuando varias operaciones son independientes, ejecutarlas en serie con await desperdicia tiempo. Promise.all recibe un array de promesas y devuelve una nueva promesa que se resuelve cuando todas se cumplen.
async function cargarTodo() {
const [user, settings] = await Promise.all([
fetchJson('/api/user'),
fetchJson('/api/settings')
]);
return {user, settings};
}
Si nos interesa el primer resultado que llegue, usamos Promise.race. Es útil para timeouts.
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Tiempo agotado')), 5000)
);
async function fetchConTimeout(url) {
return await Promise.race([fetchJson(url), timeout]);
}
5. Depuración y herramientas
Las promesas pueden ser difíciles de seguir en la consola. Algunas recomendaciones:
- Usa
console.trace()dentro decatchpara obtener la pila completa. - En Chrome, habilita la opción "Pause on exceptions" y "Pause on caught exceptions" para inspeccionar errores asíncronos.
- Utiliza
async stack traces(Node >= 12 y navegadores modernos los muestran automáticamente).
6. Buenas prácticas que hacen la diferencia
- Never swallow errors: nunca dejes un
catchvacío; al menos registra el error. - Limita el alcance de
await: no pongasawaitdentro de bucles si puedes usarPromise.all. - Tipado (cuando uses TypeScript): declara el tipo de la promesa, por ejemplo
Promise<User>, para evitar sorpresas. - Cancelación: aunque JavaScript no tiene cancelación nativa, puedes combinar
AbortControllerconfetchpara abortar peticiones largas.
7. Conclusión
Dominar promesas y async/await va más allá de saber cómo escribir await fetch(...). Implica estructurar el flujo de datos, gestionar errores de forma coherente y aprovechar la ejecución paralela cuando sea posible. Con los patrones que hemos visto, tu código será más legible, fácil de depurar y, sobre todo, más robusto frente a los imprevistos de la red.