Saltar al contenido

Errores comunes al usar async/await en JavaScript

7 min de lectura Software
  • javascript
  • async-await
  • promesas
  • software
  • desarrollo

Cuando trabajas con JavaScript moderno, la gestión de operaciones asíncronas es inevitable. async/await simplificó enormemente el manejo de promesas en comparación con las cadenas de .then(), pero no elimina la complejidad subyacente de la asincronía. Un error común al implementarlos puede llevar a código inestable o difícil de depurar.

He visto proyectos donde se adoptó async/await sin entender sus implicaciones, terminando con bugs silenciosos en producción. Resolver estos problemas consume tiempo de desarrollo que se podría haber evitado con un mejor enfoque inicial.

Manejo de errores con async/await

El error más grave al usar async/await es ignorar la gestión de errores. Una promesa rechazada dentro de una función async que no se captura, se convierte en una excepción no controlada que puede tumbar tu aplicación o dejar procesos a medias. Muchos desarrolladores olvidan que await también puede lanzar una excepción.

Mi regla es simple: toda operación asíncrona que pueda fallar debe estar dentro de un bloque try/catch. Lo mismo aplica cuando llamas a una función async externa; su resultado debe ser gestionado.

async function fetchData() {
  const response = await fetch('https://api.ejemplo.com/data');
  const data = await response.json();
  return data;
}

// Esto puede fallar silenciosamente si la API devuelve un error de red o 500
// fetchData().then(data => console.log(data)); 

La forma correcta de manejar esto es envolver la llamada en un try/catch o usar el .catch() de la promesa devuelta por la función async. La gestión de errores adecuada es clave para la robustez de la aplicación.

async function getDataSafe() {
  try {
    const data = await fetchData();
    console.log('Datos recibidos:', data);
  } catch (error) {
    console.error('Error al obtener los datos:', error.message);
    // Aquí puedes registrar el error, notificar, o devolver un valor por defecto
  }
}
getDataSafe();

Ejecutar promesas en serie cuando pueden ser paralelas

Otro error frecuente es ejecutar operaciones async en serie cuando podrían ejecutarse en paralelo. Usar await dentro de un bucle for...of procesa cada elemento uno a uno, ralentizando la ejecución total del flujo. Si las operaciones son independientes entre sí, no hay razón para esperar a que una termine antes de empezar la siguiente.

Esto no es un problema de rendimiento en sí mismo, sino de oportunidad perdida. Si tienes diez llamadas a una API que no dependen unas de otras, hacerlas en paralelo te ahorra una cantidad de tiempo lineal que se multiplica por el número de operaciones.

async function processItemsSequentially(items) {
  const results = [];
  for (const item of items) {
    // Cada 'await' pausa el bucle hasta que la promesa se resuelve
    const result = await processSingleItem(item); 
    results.push(result);
  }
  return results;
}

Para ejecutar promesas en paralelo, Promise.all() es la herramienta adecuada. Recibe un array de promesas y se resuelve cuando todas ellas lo hacen, devolviendo un array con sus resultados en el mismo orden. Si una de las promesas falla, Promise.all() se rechaza inmediatamente.

async function processItemsInParallel(items) {
  const promises = items.map(item => processSingleItem(item));
  const results = await Promise.all(promises);
  return results;
}

Olvidar el await en llamadas asíncronas

Parece obvio, pero es un error que veo a menudo: llamar a una función async y olvidar el await. Cuando esto ocurre, la función que llama no espera a que la promesa se resuelva. Continúa su ejecución inmediatamente con el valor de la promesa pendiente, no con el resultado de la operación. Esto puede llevar a que intentes operar con un valor undefined o una Promise sin resolver, provocando errores en cascada.

En proyectos donde he tenido que hacer code review para otros equipos, este es un punto recurrente. La falta de await es difícil de detectar si no se entiende bien el flujo, especialmente en sistemas que dependen de integraciones entre sistemas complejas.

async function saveUser(user) {
  // Olvidamos el 'await', la función retorna antes de que la promesa se resuelva
  // y 'database.save()' sigue ejecutándose en segundo plano
  database.save(user); 
  console.log('Usuario guardado (o eso parece)');
}

async function createUserAndLog(user) {
  saveUser(user); // No se espera aquí
  console.log('Función createUserAndLog terminada.'); // Se ejecuta antes de que saveUser termine
}

El await es lo que transforma la promesa en su resultado (o la excepción). Sin él, solo tienes la promesa en sí, un objeto que representa el futuro valor.

async function saveUserCorrectly(user) {
  await database.save(user); 
  console.log('Usuario guardado.');
}

async function createUserAndLogCorrectly(user) {
  await saveUserCorrectly(user); // Ahora esperamos
  console.log('Función createUserAndLog terminada.'); 
}

Dependencias asíncronas mal gestionadas

A veces, las operaciones asíncronas tienen dependencias: el resultado de una es necesario para la siguiente. Un error es encadenar .then() anidados o await excesivos que complican la lectura y el mantenimiento del código. Esto es lo que se conoce como “callback hell” o “promise hell” en su versión async/await.

Cuando un cliente me llega con este problema, la solución pasa por estructurar mejor el flujo. No se trata de eliminar la asincronía, sino de organizarla.

async function processOrderBad(orderId) {
  const orderDetails = await getOrderDetails(orderId);
  const user = await getUser(orderDetails.userId);
  const products = await getProducts(orderDetails.productIds);
  // ... más awaits anidados
  const finalResult = await calculateTotal(orderDetails, user, products);
  return finalResult;
}

Aunque este ejemplo usa await de forma secuencial, que es correcto si hay dependencias, el problema surge cuando se encadenan operaciones que podrían agruparse o cuando la lógica se vuelve excesivamente lineal para un problema complejo. La clave es identificar qué operaciones son realmente dependientes y cuáles pueden ejecutarse en paralelo.

Tradeoffs de una mala implementación de async/await

Lo que ganas (al usar async/await correctamente):

  • Código legible: Se parece más a código síncrono, facilitando su comprensión.
  • Manejo de errores centralizado: try/catch funciona como esperas.
  • Rendimiento optimizado: Ejecutas operaciones en paralelo cuando es posible, sin bloquear el hilo principal.
  • Fiabilidad: Las excepciones no se tragan y los flujos se completan de forma predecible.

Lo que complicas (al implementar async/await mal):

  • Bugs intermitentes: Errores difíciles de reproducir y depurar que aparecen solo en ciertas condiciones de carrera.
  • Fugas de recursos: Promesas que nunca se resuelven o rechazan, dejando conexiones abiertas o tareas pendientes.
  • Código lento: Operaciones secuenciales innecesarias que penalizan la experiencia de usuario.
  • Mantenimiento: Un infierno de await olvidados o try/catch ausentes.

Lo que no negocias:

  • Gestión de errores: Toda promesa debe tener un camino de éxito y uno de error bien definido.
  • Claridad del flujo: El código debe reflejar si una operación es secuencial o paralela.
  • Pruebas unitarias: Asegúrate de que tus funciones async se prueban para ambos casos, éxito y fracaso.

Si estás trabajando en proyectos JavaScript y quieres asegurar la calidad del código, te recomiendo leer sobre testing automatizado para ahorrar dinero y cómo puedes nombrar cosas en el código siguiendo buenas prácticas.

Lucas Juárez
Lucas Juárez

Técnico freelance especializado en desarrollo a medida, automatizaciones con IA y gestión técnica para negocios en España. Más sobre mí →

Compartir:

¿Necesitas desarrollo a medida?

Desarrollo funcionalidades específicas, integraciones entre sistemas y herramientas internas. Si se puede programar, probablemente puedo hacerlo.

Chat