Menu

async/await en JavaScript: código asíncrono legible

Cómo funciona async/await en JavaScript: funciones async, await con promesas, manejo de errores con try/catch y ejecución en paralelo con Promise.all.

async/await en JavaScript son promesas con otro traje

async/await no es un modelo de concurrencia nuevo. Es azúcar sintáctica sobre las promesas que te permite escribir código asíncrono con aspecto secuencial. La maquinaria es la misma, pero la forma es mucho más amigable.

Aquí tienes la misma tarea escrita de las dos maneras:

index.js
Output
Click Run to see the output here.

Ambas funciones devuelven una promesa. Las dos hacen exactamente lo mismo. La versión con async se lee de arriba abajo, sin encadenar .then — y ahí está toda la gracia.

async marca una función como que devuelve una promesa

Si pones async delante de una function, de una función flecha o de un método, pasan dos cosas:

  1. La función siempre devuelve una promesa. Lo que retornes con return se convierte en el valor resuelto.
  2. Puedes usar await dentro de ella.
index.js
Output
Click Run to see the output here.

Fíjate en que result no es el string: es una promesa que resuelve al string. Aunque greet no tenga ningún await ni haga trabajo asíncrono, la palabra clave async igualmente envuelve el valor de retorno en una promesa. Y si la función lanza un error, la promesa se rechaza.

await pausa la ejecución hasta que la promesa se resuelve

Dentro de una función async, await algunaPromesa detiene esa función hasta que la promesa se resuelva, y entonces te devuelve el valor resuelto. Si la promesa termina en rechazo, await lanza el error.

index.js
Output
Click Run to see the output here.

Fíjate en el orden de la salida. "cuenta atrás iniciada" se imprime antes que "2", y es que await solo pausa la función async, no el resto del programa. El event loop sigue girando; countdown simplemente retoma su ejecución más adelante, cuando cada promesa de wait se resuelve.

Puedes usar await con cualquier cosa que se comporte como una promesa. Incluso await 42 es válido: los valores que no son promesas se envuelven automáticamente en Promise.resolve(42) y se resuelven al instante.

Manejo de errores con try/catch en async/await

Con promesas clásicas encadenas .catch(). Con async/await, una promesa rechazada se convierte en una excepción lanzada que puedes capturar de la forma de siempre:

index.js
Output
Click Run to see the output here.

Un mismo try/catch cubre todos los await que haya dentro. Fallos de red, errores al parsear JSON y los throw que tú mismo lanzas caen todos en el mismo catch. Es una mejora enorme frente a encadenar .then/.catch anidados.

Ojo con un detalle: fetch solo rechaza ante errores de red, no ante respuestas HTTP 4xx/5xx. Tienes que comprobar res.ok a mano y lanzar el error tú — un patrón que vas a ver sin parar en código real.

Evita await dentro de un bucle cuando no haga falta

Esta es la trampa más típica de async/await. Usar await de forma secuencial dentro de un bucle hace que cada iteración espere a que termine la anterior:

index.js
Output
Click Run to see the output here.

sequential tarda unos 900ms. parallel, unos 300ms. La regla práctica: si las tareas no dependen del resultado de las otras, lánzalas todas a la vez y luego haz await Promise.all. Usa await una a una solo cuando la siguiente llamada realmente necesite el resultado de la anterior.

Para colecciones, el patrón habitual es Promise.all(items.map(async (x) => ...)). Un for...of normal con await dentro se ejecuta en serie: a veces es justo lo que quieres (para limitar el ritmo de las peticiones o mantener el orden), pero la mayoría de las veces no.

Combinar async/await con promesas tradicionales

No tienes que elegir bando. Las funciones async devuelven promesas, y await funciona con cualquier promesa, así que puedes mezclarlos sin problema:

index.js
Output
Click Run to see the output here.

Ambos estilos son intercambiables. Usa await cuando el código se lea mejor de arriba hacia abajo, y .then cuando quieras algo puntual y rápido o cuando estés trabajando fuera de un contexto async.

await en el nivel superior (dentro de módulos ES)

Antes tenías que envolver cualquier await dentro de una función async, porque no estaba permitido usarlo en el nivel superior de un script. Eso ya cambió: dentro de un módulo ES (un archivo .mjs o un <script type="module">), puedes usar await directamente en el nivel superior:

// in an ES module
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
const user = await res.json();
console.log(user.name);

El await de nivel superior (top level await) retrasa la finalización del módulo hasta que la promesa en cuestión se resuelva, y cualquiera que importe ese módulo también tendrá que esperar. Resulta muy útil para cargar configuraciones o para imports dinámicos, pero conviene usarlo con moderación: un await lento en el nivel superior bloquea a todo el que importe ese módulo.

En archivos CommonJS o en scripts clásicos en línea, esto sigue dando un SyntaxError. El truco de toda la vida es envolverlo en una función async autoinvocada:

index.js
Output
Click Run to see the output here.

Errores comunes con async/await en JavaScript

Un repaso rápido a los tropiezos más habituales:

  • Olvidarse del async. Usar await dentro de una función normal es un error de sintaxis. La solución es añadir async a la función, o bien llamar a la función asíncrona con .then.
  • Olvidarse de poner await al resultado. const data = getJSON(url); te devuelve una promesa, no los datos. Si la tratas como si fuera el valor, verás aparecer [object Promise] en la salida.
  • Rechazos sin gestionar. Una función async que lanzas y olvidas (doWork();) se va a tragar los errores en silencio, salvo que le añadas un .catch o la envuelvas con await dentro de un try/catch.
  • forEach con callbacks asíncronos. array.forEach(async (x) => await something(x)) no espera absolutamente nada: forEach ignora las promesas que devuelve el callback. Usa for...of con await, o bien Promise.all(array.map(...)).
index.js
Output
Click Run to see the output here.

Ejecútalo: "¿terminado?" aparece antes que cualquier "listo", porque roto retorna sin esperar. En cambio, arreglado espera a que todo termine y recién entonces imprime "¡terminado!" al final.

Cuándo usar async/await

Usa async/await por defecto en cualquier código que encadene varios pasos asíncronos en secuencia, o que necesite manejo de errores al estilo try/catch. Quédate con promesas "a pelo" para one-liners triviales, para código de librería que devuelve una promesa sin necesitar esperarla, o cuando realmente necesites combinadores como Promise.race o .finally() dentro de una cadena.

Bien aprovechado, async/await hace que el código asíncrono se lea como una receta: haz esto, luego esto, luego esto otro. El event loop sigue funcionando igual por debajo; tú simplemente dejas de pensar en callbacks.

Siguiente paso: la API fetch

La mayoría de los ejemplos de esta página usaron fetch como comodín para "alguna operación asíncrona". Vale la pena mirarlo en serio: cómo funcionan las peticiones y respuestas, cómo manejar JSON, cómo configurar headers y por qué fetch no rechaza ante errores HTTP. Eso lo vemos en la siguiente página.

Preguntas frecuentes

¿Qué hace async/await en JavaScript?

async/await es azúcar sintáctico sobre las promesas que te permite escribir código asíncrono como si fuera síncrono. async marca una función para que devuelva siempre una promesa, y await pausa la ejecución dentro de esa función hasta que la promesa se resuelva (o se rechace), devolviéndote el valor resuelto. Por debajo siguen siendo promesas; simplemente se leen mucho mejor.

¿Se puede usar await fuera de una función async?

En el nivel superior de un módulo ES, sí: es lo que se conoce como top-level await. Dentro de funciones normales o en scripts CommonJS, no: usar await fuera de una función async es un error de sintaxis. La solución habitual es envolver el código en una función async y llamarla, o convertir el archivo en un módulo ES.

¿Cómo se manejan los errores con async/await?

Envuelve las llamadas con await en un bloque try/catch. Cuando una promesa que estás esperando se rechaza, se convierte en una excepción que el catch puede capturar. Para tareas en segundo plano que no estés esperando con await, añádeles un .catch() a la promesa devuelta para que los rechazos no queden sin gestionar.

¿await bloquea todo el programa?

No. await solo pausa la función async en la que estás. El event loop sigue funcionando con normalidad: los timers se disparan, otras tareas asíncronas avanzan y la interfaz sigue respondiendo. Quien llamó a la función recibe al instante una promesa pendiente y continúa su ejecución.

Aprende a programar con Coddy

COMENZAR