Tutorial de JavaScript moderno: promesas, async/await y APIs

Última actualización: 04/12/2025
Autor: Isaac
  • Async/await se apoya en promesas para ofrecer un código asíncrono más legible, cercano al estilo síncrono tradicional.
  • Las funciones async siempre devuelven una promesa y permiten usar await para pausar su ejecución sin bloquear el hilo principal.
  • El manejo de errores con try/catch y utilidades como Promise.all hacen más sencillo controlar fallos y combinar varias operaciones en paralelo.
  • El uso conjunto de promesas, async/await y APIs como fetch es la base del desarrollo moderno en JavaScript orientado a red.

js

La programación asíncrona en JavaScript se ha vuelto imprescindible en el desarrollo web moderno: peticiones a APIs, acceso a bases de datos, lectura de archivos o cualquier operación que tarde más de un instante dependen de ella para que la interfaz siga siendo fluida. Gracias a las promesas, async/await y las APIs tenemos un conjunto de herramientas muy potente para controlar ese flujo sin bloquear la aplicación.

Durante años se utilizó sobre todo el patrón de callbacks y promesas encadenadas, que aunque funcionan muy bien, pueden terminar en el famoso “callback hell” o en cadenas infinitas de .then() y .catch() difíciles de seguir. Con la llegada de async/await en ECMAScript 2017, JavaScript dio un salto enorme en legibilidad, permitiendo escribir código asíncrono con un estilo casi idéntico al código síncrono de toda la vida.

Qué es la programación asíncrona en JavaScript y por qué es tan importante

La programación asíncrona es una forma de organizar el código para que una tarea pueda iniciarse y el programa continúe ejecutando otras cosas sin quedarse “congelado” esperando la respuesta. En JavaScript esto es vital porque el lenguaje se ejecuta, en el navegador y en Node.js, sobre un único hilo principal que atiende tanto la lógica como la interfaz.

Cuando lanzas una petición HTTP a una API, lees un archivo o consultas una base de datos, esas operaciones pueden tardar desde milisegundos hasta varios segundos. Si fuesen síncronas, el usuario vería la página bloqueada sin poder interactuar. Con el modelo asíncrono, el motor de JavaScript delega esas tareas y mientras tanto puede seguir pintando la UI, manejando eventos y ejecutando otros scripts.

Originalmente, el mecanismo estándar para reaccionar cuando una operación terminaba era pasar una función callback como argumento. Esa función se ejecutaba más tarde con el resultado o el error. El problema es que, encadenando callbacks dentro de otros callbacks, se produce el temido callback hell: código con demasiada indentación, difícil de leer, de depurar y de mantener.

Para solucionar parte de ese caos aparecieron las promesas, que encapsulan el resultado de una operación asíncrona y permiten encadenar acciones con .then() y gestionar fallos con .catch(). Esto ya fue un gran avance, pero seguía dejando el código lleno de callbacks “disfrazados” detrás de esos métodos.

Promesas: la base de async/await

Antes de entender async/await, hay que tener muy claro qué es una promesa en JavaScript. Una promesa es un objeto que representa el resultado futuro de una operación asíncrona: algo que todavía no ha pasado pero pasará más adelante, con éxito o con error.

Una promesa estándar puede encontrarse en uno de tres estados bien definidos: pendiente (pending) mientras la operación está en curso, cumplida (fulfilled) cuando termina bien y produce un valor, o rechazada (rejected) cuando falla y genera un error. Ese estado de la promesa determina qué callbacks se ejecutarán a continuación.

Cuando creas una promesa usando el constructor Promise, le pasas una función ejecutora que recibe dos argumentos: resolve y reject. Si la operación va bien, llamas a resolve(valor); si ocurre un problema, invocas reject(error). Después, desde fuera, puedes usar miPromesa.then() para procesar el resultado y .catch() para gestionar los errores.

Además de las promesas creadas a mano, muchas APIs modernas del lenguaje ya devuelven una promesa de serie. La función fetch(), por ejemplo, inicia una solicitud HTTP y retorna inmediatamente una promesa que terminará resuelta con un objeto Response o rechazada si hay fallo de red.

Para trabajar con varias tareas en paralelo, JavaScript ofrece utilidades como Promise.all(), que recibe un array de promesas y devuelve una nueva promesa que se resuelve solo cuando todas han completado correctamente, o se rechaza si cualquiera de ellas falla. Esto es especialmente útil cuando quieres combinar datos de distintas fuentes antes de seguir.

  iPhone Disconnects from WiFi When Locked or in Sleep Mode

Conceptos clave: async, await y el modelo de errores

La sintaxis de async/await se construye sobre las promesas, no las reemplaza. Lo que hace es proporcionar una forma más cómoda, clara y cercana a la programación síncrona para trabajar con ellas, reduciendo el ruido visual de .then() encadenados.

La palabra clave async se coloca delante de una función para indicar al motor de JavaScript que esa función va a trabajar de manera asíncrona y que, pase lo que pase dentro, siempre devolverá una promesa. Aunque devuelvas un valor normal, el motor lo envolverá automáticamente en Promise.resolve(valor).

Por su parte, la palabra reservada await solo puede utilizarse dentro de una función declarada como async. Su función es pausar la ejecución dentro de esa función hasta que la promesa que se le pasa se resuelva o se rechace, devolviendo directamente el valor resuelto o lanzando una excepción si la promesa termina con error.

Cuando usas await promesa y la promesa se resuelve, obtienes el resultado como si se tratara de una llamada síncrona. Si en su lugar la promesa se rechaza, se genera una excepción en ese punto, equivalente a hacer un throw error. Eso permite reutilizar las mismas estructuras de gestión de errores que en el código síncrono, como try..catch.

Al envolver nuestras llamadas asíncronas en un bloque try { ... } catch (error) { ... }, podemos capturar los casos en los que alguna de las promesas falle, mostrar mensajes adecuados al usuario o tomar decisiones como reintentar, redirigir o registrar el fallo en un sistema externo.

La palabra clave async: funciones que siempre devuelven promesas

Colocar async delante de una función tiene dos consecuencias muy importantes: por un lado, fuerza a que el valor de retorno sea siempre una promesa; por otro, habilita el uso de await dentro del cuerpo de esa función, lo que cambia por completo cómo se estructura el flujo asíncrono.

Si declaras algo como async function sumar() { return 1; }, aunque parezca una función normal, lo que en realidad obtienes es una función que devuelve una promesa ya resuelta con el valor 1. En el exterior tendrás que acceder a ese resultado usando await sumar() o con sumar().then() si prefieres el estilo encadenado.

También puedes devolver explícitamente una promesa desde una función async, por ejemplo return Promise.resolve(1);, y el comportamiento será el mismo. En ambos casos, la función queda alineada con el modelo de promesas, lo que facilita su combinación con otras funciones asíncronas y con utilidades como Promise.all().

Es muy habitual que, cuando estamos empezando, se nos olvide marcar una función como async y aún así usemos await dentro. En esa situación, JavaScript lanzará un error de sintaxis indicando que await solo es válido en funciones asíncronas (o en módulos que lo soportan a nivel superior). Este mensaje es un recordatorio de que async y await siempre van de la mano.

En entornos donde no utilizamos módulos o tenemos que dar soporte a navegadores antiguos, existe un patrón muy extendido que consiste en envolver el código dentro de una función async autoejecutable, algo como (async () => { ... })();. De este modo, todo el bloque queda dentro de un contexto asíncrono sin “contaminar” el espacio global.

Cómo funciona await y qué hace realmente con las promesas

El verdadero cambio de chip llega con await. Cuando el motor de JavaScript se encuentra con const resultado = await algunaPromesa; dentro de una función async, pausa la ejecución de esa función en ese punto y vuelve al bucle de eventos para seguir atendiendo otras tareas.

Mientras tanto, la promesa continúa su curso de manera independiente. Cuando finalmente se resuelve, el motor reanuda la ejecución de la función justo después de la línea con await, asignando a la variable el valor devuelto por la promesa. Desde el punto de vista de quien lee el código, parece una llamada síncrona que simplemente tarda un poco más.

  Safari Tabs Disappearing on iPhone: Methods to Repair?

Lo interesante es que esta pausa no bloquea la CPU. El hilo principal de JavaScript puede procesar eventos de usuario, animaciones, otro código asíncrono, etc. Por eso decimos que await es más una abstracción sintáctica elegante sobre las promesas que una forma nueva de ejecutar tareas en segundo plano.

Si lo prefieres, puedes imaginar que la combinación const resultado = await promesa; es promesa.then(valor => { resultado = valor; ... }) de algo equivalente, pero distribuido de una forma lineal que evita los callbacks anidados y mejora la claridad del flujo.

Un detalle curioso es que, si pasas a await un objeto que no es una promesa pero tiene un método .then(), el motor intentará tratarlo como si lo fuera. Invocará ese .then() con dos funciones internas de resolución y rechazo muy similares a las del constructor de Promise. De esta manera, await es compatible con implementaciones de promesas “tipo thenable”.

Manejo de errores con async/await: try/catch y promesas rechazadas

Cuando una promesa se rechaza mientras estás usando await, el efecto directo es que se lanza una excepción en esa línea. Desde el punto de vista de tu código, es como si hubieras escrito throw error; justo donde está el await, lo que hace que esa ejecución salte al bloque catch más cercano.

Por ejemplo, si usas const datos = await obtenerDatos(); y la función obtenerDatos devuelve una promesa que termina en error, el flujo irá directamente al bloque catch (error) de tu try..catch. Si no tienes un bloque de captura alrededor, la promesa implícita que devuelve la función async quedará rechazada.

Esto quiere decir que, aunque por dentro siga habiendo promesas, cuando trabajas con async/await la forma natural de manejar errores es con try..catch, como en cualquier otro código JavaScript que lance excepciones. Esta unificación de estilos simplifica mucho la lógica y reduce la necesidad de recordar distintos patrones según sea código síncrono o asíncrono.

Si, por despiste, se te olvida añadir un .catch() a la promesa devuelta por una función async que falla, el entorno mostrará un aviso de “promesa no manejada” (unhandled promise rejection). En el navegador, además, puedes usar el evento global unhandledrejection para registrar un manejador que capture todos esos errores y evitar que pasen desapercibidos.

Cuando combinas await con utilidades como Promise.all, un solo error en una de las promesas se propaga a la promesa conjunta y finalmente se traduce en una excepción que puedes capturar con try..catch. De esta forma, gestionar errores en flujos complejos sigue siendo coherente y centralizado.

Async/await con fetch: llamadas a APIs paso a paso

Uno de los usos más habituales de async/await es el acceso a APIs HTTP usando la función fetch(). Esta API devuelve una promesa que se resuelve con un objeto Response, sobre el que luego puedes invocar métodos como .json() o .text(), que a su vez devuelven nuevas promesas.

Imagina que quieres encapsular la lógica de pedir datos a un endpoint en una función. Si declaras async function obtenerDatos(), dentro de ella puedes esperar la respuesta de red con await fetch(...) y, seguidamente, transformar el cuerpo en JSON con const datos = await respuesta.json(); sin necesidad de encadenar varios .then().

Para asegurar que el servidor ha respondido correctamente, es frecuente comprobar la propiedad respuesta.ok, que indica si el código de estado HTTP está en el rango 200-299. Si no es así, puedes lanzar manualmente un error con throw new Error('Error en la red');, lo que hará que el catch capture la situación.

Organizar todo el flujo dentro de un try { ... } catch (error) { ... } hace que la función asíncrona sea mucho más resistente: cualquier problema, ya sea de red, de parseo de JSON o de lógica, activará el bloque de captura donde puedes registrar el fallo o mostrar un mensaje más amigable.

Al final, para usar la función basta con llamarla como a cualquier otra: obtenerDatos();. Recuerda que, al ser asíncrona, devuelve una promesa, así que desde fuera tendrás que elegir si usar await obtenerDatos() dentro de otra función async o manejarla con .then().catch() si estás en un contexto no asíncrono.

  Error corregido en Windows 10 wdf01000.sys.

Ejecutar varias operaciones en paralelo con Promise.all y async/await

Muchas veces no quieres esperar a que termine una petición para iniciar la siguiente, sino que te interesa lanzar varias operaciones asíncronas a la vez y continuar cuando todas hayan finalizado. Para eso, el método Promise.all() encaja a la perfección.

Supón que necesitas consumir dos APIs distintas, por ejemplo /datos1 y /datos2, y luego fusionar la información en un único objeto. En lugar de esperar a que se complete la primera para empezar la segunda, puedes escribir algo como const [r1, r2] = await Promise.all([fetch(url1), fetch(url2)]); y ambas solicitudes se realizarán en paralelo.

Una vez tengas las dos respuestas, puedes aplicar await sobre cada una para transformarlas en JSON: const d1 = await r1.json(); const d2 = await r2.json();. Finalmente, es muy común combinar los datos con el operador de propagación: const datosCombinados = { ...d1, ...d2 };, lo que genera una estructura única que mezcla las propiedades de ambas.

Todo este bloque debería ir envuelto en un try..catch, ya que cualquier fallo en una de las peticiones hará que Promise.all() se rechace. Gracias a async/await, manejar este tipo de escenarios con operaciones concurrentes resulta visualmente sencillo pero muy potente en cuanto a rendimiento.

Este patrón es especialmente útil cuando tu aplicación web requiere varios recursos independientes antes de poder renderizar una vista: por ejemplo, obtener configuración, datos de usuario y estadísticas de uso al mismo tiempo, reduciendo la espera total.

Ventajas reales de async/await frente a promesas “puras” y callbacks

El uso de async/await aporta una serie de beneficios prácticos que se notan desde los primeros ejemplos hasta aplicaciones de gran tamaño. El más evidente es la legibilidad: el flujo queda escrito de arriba abajo sin necesidad de anidar funciones ni encadenar .then() uno tras otro.

Otro punto fuerte es la mantenibilidad del código. Al tener bloques más lineales, resulta más fácil introducir cambios, extraer partes en funciones auxiliares o reutilizar lógica. Esto repercute directamente en menos errores y en una curva de aprendizaje más suave para personas que se incorporan al proyecto.

El manejo de errores con try..catch en lugar de múltiples .catch() dispersos ayuda a centralizar la lógica de fallos. Puedes rodear varios await con un solo bloque e implementar una política coherente de gestión de errores, sin perder el detalle de en qué línea ocurrió el problema.

Desde el punto de vista del rendimiento, async/await no hace que el código sea “más rápido” por sí mismo, pero sí puede animarte a usar patrones como Promise.all() para ejecutar tareas en paralelo donde antes quizá las lanzabas en serie. El resultado es menos tiempo de espera total para el usuario final.

Además, async/await es compatible con las promesas tradicionales; no tienes que reescribir todo tu código para adoptarlo. Puedes combinar funciones que usen .then() con otras que utilicen await sin problema, lo que facilita migrar proyectos poco a poco.

En el ecosistema moderno, tanto en navegador como en Node.js, la mayoría de herramientas y librerías ya se apoyan en promesas y async/await. Esto hace que aprender bien estos conceptos no sea opcional, sino un requisito para trabajar con JavaScript actual en proyectos profesionales.

Todo este conjunto de ideas —promesas, async/await, fetch, Promise.all, try/catch y el glosario de términos clave— conforma un marco muy sólido para escribir código asíncrono claro, legible y robusto. Dominarlos te permite construir aplicaciones web modernas que responden rápido, aprovechan bien la red y son mucho más fáciles de mantener con el paso del tiempo.