async/await no hace concurrente tu código: el modelo para ordenar, paralelizar y manejar fallos

Es posible escribir una función con async y varios await y seguir ejecutando las operaciones en una secuencia innecesariamente lenta. También es posible envolver una promesa en try/catch y no tener una política clara para los fallos parciales. La sintaxis moderna hace que el código sea más legible, pero no decide por ti qué debe esperar, qué puede empezar al mismo tiempo ni qué resultado conservar cuando una tarea falla.

La idea central de la programación asíncrona en JavaScript no es memorizar callbacks, promises y async/await como seis conceptos separados. Es aprender a representar una operación que terminará después y a coordinarla con el resto del programa. La guía de JavaScript asíncrono de MDN presenta justamente esa progresión: operaciones síncronas y asíncronas, promises, funciones async, APIs basadas en promises y tareas que pueden ejecutarse sin bloquear la interfaz.

“Esperar” no significa siempre lo mismo

Cuando una función usa await, la ejecución de esa función queda suspendida hasta que la promise se cumpla o se rechace. El resto del entorno puede seguir procesando otros eventos, pero tu función no avanzará a la siguiente línea. Esa pausa puede ser exactamente lo que necesitas cuando la segunda operación depende del resultado de la primera.

const usuario = await obtenerUsuario(id);
const permisos = await obtenerPermisos(usuario.id);
mostrarPanel(usuario, permisos);

Aquí hay una relación de dependencia: no puedes pedir los permisos del usuario correcto hasta conocer su identificador. Ejecutar ambas llamadas al mismo tiempo cambiaría el problema. El orden es parte de la lógica, no un defecto de rendimiento.

La confusión aparece cuando dos operaciones no dependen entre sí:

const categorias = await obtenerCategorias();
const noticias = await obtenerNoticias();
mostrarInicio(categorias, noticias);

Si una llamada no necesita el resultado de la otra, puedes iniciar ambas antes de esperar sus resultados:

const categoriasPromise = obtenerCategorias();
const noticiasPromise = obtenerNoticias();

const [categorias, noticias] = await Promise.all([
  categoriasPromise,
  noticiasPromise,
]);

mostrarInicio(categorias, noticias);

La diferencia no está en sustituir una palabra por otra. Está en declarar la relación real entre las tareas. Si quieres repasar la base del lenguaje antes de continuar, nuestra guía de conceptos básicos de JavaScript puede servir como punto de partida.

El event loop explica qué se libera y qué sigue bloqueando

En el navegador y en Node.js, la asincronía permite que ciertas operaciones de entrada y salida progresen sin mantener bloqueado el flujo principal. La documentación oficial de Node.js explica que su event loop puede delegar operaciones al sistema y volver a ejecutar callbacks cuando hay resultados disponibles [documentación del event loop de Node.js]. Pero esa capacidad no convierte cualquier cálculo en trabajo paralelo.

Una petición de red puede estar esperando fuera del código JavaScript mientras la aplicación atiende otros eventos. Un bucle que consume la CPU durante mucho tiempo sigue ocupando el hilo cuando su callback comienza. Añadir await delante de una función síncrona pesada no la vuelve liviana:

const resultado = await calcularMillonesDeVeces();

Si calcularMillonesDeVeces() no devuelve una promise que delega trabajo, el cálculo puede bloquear el hilo antes de que exista algo que esperar. Para tareas de CPU intensiva, considera dividir el trabajo, usar un worker o moverlo a un proceso apropiado. La asincronía es una forma de coordinar esperas; no es una promesa de paralelismo automático.

Callbacks muestran el problema histórico

Las APIs antiguas suelen recibir una función que se ejecutará cuando termine una operación. El patrón es válido, pero al encadenar muchos pasos puede generar una estructura que oculta el orden, mezcla errores y dificulta reutilizar resultados:

leerConfiguracion((error, configuracion) => {
  if (error) return manejar(error);
  cargarUsuario(configuracion, (error, usuario) => {
    if (error) return manejar(error);
    cargarPreferencias(usuario, (error, preferencias) => {
      if (error) return manejar(error);
      mostrar(usuario, preferencias);
    });
  });
});

El problema no es que una callback sea siempre incorrecta. El problema es que la forma de anidar operaciones empieza a mezclar tres preguntas: cuándo termina cada tarea, cómo pasa el resultado y quién maneja el error. Las promises separan mejor esas responsabilidades y permiten componer operaciones con métodos como then, catch y finally.

Una promise representa un resultado que todavía no tienes

Una promise puede estar pendiente, cumplirse o rechazarse. Esa representación permite que una función entregue inmediatamente un objeto que describe un resultado futuro, en lugar de bloquear hasta que la operación termine. La función que consume la promise todavía debe decidir qué hará con cada estado.

obtenerPerfil(id)
  .then((perfil) => mostrarPerfil(perfil))
  .catch((error) => mostrarError(error));

async/await ofrece una sintaxis que suele parecerse más a una secuencia tradicional:

async function cargarPerfil(id) {
  try {
    const perfil = await obtenerPerfil(id);
    mostrarPerfil(perfil);
  } catch (error) {
    mostrarError(error);
  }
}

La función es más fácil de leer, pero las decisiones siguen ahí. Debes saber qué promesa puede fallar, si el error puede recuperarse, si hay que reintentar y qué parte de la interfaz queda válida. La sintaxis no reemplaza ese diseño.

Cuando varias respuestas llegan juntas, también necesitas una forma clara de representarlas. Un array puede conservar el orden de una colección de resultados y un objeto puede asociar cada dato con una clave estable; nuestra guía sobre objetos en JavaScript ayuda a repasar esa decisión antes de diseñar el flujo asíncrono.

Elige la composición según la relación entre tareas

SituaciónComposición razonablePregunta que debes responder
La tarea B necesita el resultado de AEsperar A y después iniciar B¿Qué dato de A necesita B?
Varias tareas son independientes y todas son necesariasIniciarlas y usar Promise.all()¿Qué hago si una falla?
Necesitas observar cada resultado aunque alguno falleUsar Promise.allSettled()¿Cómo separo éxito y rechazo?
La primera respuesta válida es suficienteEvaluar una composición como Promise.any()¿Qué significa “válida” en este caso?
La operación debe detenerse si el usuario abandonaDiseñar cancelación con la API disponible¿Quién cancela y qué recursos se liberan?

La referencia de Promise.all() en MDN explica que el método cumple cuando todas las promises cumplen y rechaza cuando una de ellas se rechaza. Ese comportamiento fail-fast es útil cuando el resultado necesita todos los datos, pero puede ser incorrecto para un panel que debe mostrar lo que sí llegó.

const resultados = await Promise.allSettled([
  cargarRecomendaciones(),
  cargarNoticias(),
  cargarAlertas(),
]);

for (const resultado of resultados) {
  if (resultado.status === "fulfilled") {
    renderizarBloque(resultado.value);
  } else {
    registrarFallo(resultado.reason);
  }
}

No uses allSettled por reflejo. Si la aplicación no puede continuar sin autenticación, mostrar una pantalla parcial puede ser engañoso. Si las recomendaciones son opcionales, detener todo por una sola respuesta fallida puede empeorar la experiencia. La política de error pertenece a la importancia de cada dato.

try/catch debe acompañar una recuperación concreta

Un bloque try/catch no vuelve segura una función por el mero hecho de existir. Dentro del catch, decide si mostrar un mensaje, reintentar, usar datos en caché, registrar el fallo o propagarlo a una capa superior. Si haces todo a la vez, puedes ocultar la causa y dejar la interfaz en un estado ambiguo.

async function cargarDatos() {
  try {
    return await obtenerDatos();
  } catch (error) {
    if (error.name === "AbortError") {
      return null;
    }

    registrarFallo(error);
    throw error;
  }
}

El código distingue una cancelación esperada de un fallo que merece ser propagado. En una aplicación real, también podrías clasificar códigos HTTP, limitar reintentos y evitar que los logs incluyan información sensible. El mismo principio vale al trabajar con el DOM: una solicitud asíncrona no debe actualizar una interfaz que ya fue desmontada. Nuestra guía de manipulación del DOM con JavaScript puede ayudarte a conectar el resultado de una operación con un elemento que todavía existe.

El orden visual necesita una decisión explícita

Cuando varias operaciones terminan en momentos diferentes, evita que cada callback cambie la interfaz por su cuenta. Decide si quieres mostrar bloques progresivamente, esperar un conjunto mínimo o mantener un estado de carga hasta que todo esté listo. Si una respuesta antigua llega después de que el usuario cambió de búsqueda, puede sobrescribir datos nuevos.

Una solución robusta identifica la solicitud vigente, cancela la anterior cuando la API lo permite o ignora el resultado que ya no corresponde. También comunica los estados: cargando, listo, vacío y error no son intercambiables. La asincronía se vuelve visible para el usuario cuando el programa no sabe qué resultado tiene autoridad.

Practica con una API lenta y con respuestas que llegan en orden diferente. Si el programa solo funciona cuando el servidor responde rápido, no has probado la coordinación; has probado una coincidencia de tiempos.

Secuencia práctica para elegir una estrategia

  1. Lista las operaciones y escribe qué dato necesita cada una.
  2. Dibuja las dependencias: una cadena indica secuencia; ramas independientes pueden comenzar juntas.
  3. Define qué resultados son obligatorios y cuáles son opcionales.
  4. Elige entre espera secuencial, Promise.all(), allSettled() u otra composición.
  5. Describe qué ocurre con timeout, cancelación, respuesta vacía y error parcial.
  6. Prueba respuestas lentas y desordenadas antes de optimizar la apariencia.

Esta secuencia transforma una pregunta vaga —“¿debo usar async/await?”— en decisiones que sí pueden revisarse. El lenguaje ofrece herramientas; el diseño del flujo determina cómo se combinan.

Preguntas que suelen mezclar conceptos

¿async/await es una forma diferente de hacer que el código sea asíncrono?

Es una sintaxis para consumir promises de forma más legible. No convierte por sí sola una función síncrona pesada en una tarea no bloqueante ni decide cómo coordinar varias operaciones.

¿Dos await consecutivos siempre son un problema de rendimiento?

No. Si la segunda operación depende de la primera, el orden es necesario. Si son independientes, iniciar ambas antes de esperar sus resultados puede evitar una espera secuencial innecesaria.

¿Promise.all() siempre es mejor que ejecutar tareas una a una?

No. Es adecuado cuando las tareas son independientes y necesitas que todas cumplan. Si una falla, el resultado agregado rechaza; además, iniciar muchas tareas puede sobrecargar un servicio o consumir recursos.

¿Qué diferencia hay entre Promise.all() y Promise.allSettled()?

Promise.all() rechaza cuando una entrada rechaza. Promise.allSettled() espera a que todas terminen y permite examinar por separado los cumplimientos y rechazos.

¿await bloquea todo JavaScript?

Suspende la función async que lo contiene, pero no significa que todo el entorno se detenga mientras espera una promise. Sin embargo, el trabajo síncrono pesado que ocurre antes o después puede bloquear el hilo principal.

¿Cómo practico la asincronía si no tengo un servidor real?

Crea promises con retrasos controlados, cambia el orden de las respuestas y simula rechazos. Lo importante es observar dependencias, estados y políticas de error, no la velocidad de tu conexión.

La sintaxis asíncrona se vuelve clara cuando el flujo está bien pensado. Antes de añadir otro await, pregunta si estás expresando una dependencia real, si podrías iniciar una tarea independiente antes y qué debe ocurrir cuando una respuesta no llega. Esas tres preguntas valen más que memorizar una lista de métodos.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio