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

Close-up of colorful programming code displayed on a monitor screen.
Foto de Myburgh Roux en Pexels

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 de catch para 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 catch vacío; al menos registra el error.
  • Limita el alcance de await: no pongas await dentro de bucles si puedes usar Promise.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 AbortController con fetch para 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.