La async/await es una de las formas más claras y legibles de trabajar con código asíncrono en JavaScript. Antes de llegar a ella, es importante entender el problema que resuelve: la asincronía. Muchas operaciones como peticiones HTTP, lecturas de archivos o temporizadores no se completan de forma inmediata. Si usamos código síncrono, el programa se queda bloqueado esperando la respuesta. Las promesas y, sobre todo, la async/await nos permiten escribir ese código de manera secuencial sin bloquear el hilo principal.
Imagina que quieres obtener datos de una API, procesarlos y luego guardar el resultado. Sin las herramientas adecuadas, el código se convierte rápidamente en una cadena de callbacks anidados (callback hell). Las promesas llegaron para solucionar eso.
Qué es una promesa
Las promesas (Promise) se estandarizaron en ECMAScript 2015. Una promesa representa un valor que puede estar disponible ahora, en el futuro o nunca. Cada promesa puede estar en uno de tres estados:
- Pendiente (pending): es el estado inicial, ni cumplida ni rechazada.
- Cumplida (fulfilled): la operación terminó con éxito y tiene un valor.
- Rechazada (rejected): la operación falló y tiene una razón (un error).
Una vez que una promesa pasa a cumplida o rechazada, su estado ya no cambia.
Veamos cómo crear una promesa simple:
const miPromesa = new Promise((resolve, reject) => {
setTimeout(() => {
const exito = true;
if (exito) {
resolve("Datos obtenidos correctamente");
} else {
reject(new Error("No se pudieron obtener los datos"));
}
}, 1500);
});
El constructor recibe una función ejecutora que a su vez recibe dos funciones: resolve y reject. Llamamos a una u otra según el resultado de la operación.
then, catch y finally
Para consumir una promesa usamos los métodos then, catch y finally.
miPromesa
.then((resultado) => {
console.log(resultado); // "Datos obtenidos correctamente"
return "Procesando datos...";
})
.then((mensaje) => {
console.log(mensaje);
})
.catch((error) => {
console.error(error.message);
})
.finally(() => {
console.log("Operación terminada");
});
then recibe el valor cuando la promesa se cumple. Puedes encadenar varios then porque cada uno devuelve una nueva promesa. catch captura cualquier error que ocurra en la cadena. finally se ejecuta siempre, tanto si la promesa se cumple como si se rechaza, y es ideal para limpiar recursos.
async y await
La async/await se estandarizó en ECMAScript 2017 y está construida sobre promesas. La palabra clave async se coloca delante de una función y hace que esta siempre devuelva una promesa. Dentro de una función async puedes usar await delante de cualquier promesa para pausar la ejecución hasta que se resuelva.
Reescribamos el ejemplo anterior usando async/await:
async function obtenerDatos() {
try {
const resultado = await miPromesa;
console.log(resultado);
const mensaje = await procesarDatos(resultado);
console.log(mensaje);
} catch (error) {
console.error(error.message);
} finally {
console.log("Operación terminada");
}
}
function procesarDatos(datos) {
return Promise.resolve("Procesando datos...");
}
obtenerDatos();
El código se lee de arriba hacia abajo como si fuera síncrono, aunque sigue siendo asíncrono. Una función async siempre devuelve una promesa, por eso puedes usar await también sobre llamadas a otras funciones async.
Gestionar errores con try/catch
Cuando usas async/await, los errores se gestionan con el bloque try/catch, igual que en código síncrono. Cualquier rechazo de una promesa dentro de un await se convierte en una excepción que puedes capturar.
async function cargarUsuario(id) {
try {
const respuesta = await fetch(`/api/usuarios/${id}`);
if (!respuesta.ok) {
throw new Error("Usuario no encontrado");
}
const usuario = await respuesta.json();
console.log(usuario);
} catch (error) {
console.error("Error al cargar usuario:", error.message);
}
}
Este enfoque es más intuitivo que encadenar múltiples catch. Recuerda que si no capturas el error, la promesa devuelta por la función async se rechazará.
Varias tareas a la vez
A veces necesitamos ejecutar varias promesas simultáneamente. JavaScript ofrece varios métodos estáticos en la clase Promise para ello.
| Método | Qué devuelve | Cuándo falla |
|---|---|---|
| Promise.all | Array con los valores de todas las promesas (en orden) | Con el primer rechazo (se rechaza inmediatamente) |
| Promise.allSettled (ES2020) | Array de objetos con {status, value} o {status, reason} para cada promesa | Nunca (espera a que todas terminen) |
| Promise.race | El valor o error de la primera promesa que termine | Si la primera que termina es un rechazo |
| Promise.any (ES2021) | El valor de la primera promesa que se cumpla | Con AggregateError si todas fallan |
Ejemplo práctico:
const promesa1 = fetch("/api/usuarios").then(r => r.json());
const promesa2 = fetch("/api/productos").then(r => r.json());
const promesa3 = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 5000));
async function cargarTodo() {
try {
// Espera a que todas se cumplan
const [usuarios, productos] = await Promise.all([promesa1, promesa2]);
console.log("Usuarios:", usuarios);
console.log("Productos:", productos);
// Espera a todas aunque algunas fallen
const resultados = await Promise.allSettled([promesa1, promesa2, promesa3]);
console.log(resultados);
// La primera que termine (éxito o error)
const primero = await Promise.race([promesa1, promesa3]);
console.log(primero);
} catch (error) {
console.error(error);
}
}
cargarTodo();
En módulos ES también puedes usar await en el nivel superior (top-level await, ES2022), lo que resulta útil en archivos de configuración o scripts modernos.
Errores frecuentes
- Olvidar await: si llamas a una función
asyncsinawaitobtienes una promesa en lugar del valor. El código continúa ejecutándose antes de que la promesa se resuelva. - Usar await dentro de bucles de forma secuencial sin querer: si no necesitas esperar a cada iteración, usa
Promise.allcon un array de promesas para ejecutarlas en paralelo. Si realmente necesitas secuencialidad, elawaitdentro del bucle es correcto, pero ten en cuenta el impacto en el rendimiento. - No capturar rechazos: las promesas rechazadas que no tienen un
catcho untry/catchalrededor delawaitgeneran un aviso de unhandled rejection tanto en navegadores como en Node.js. Siempre gestiona los errores.
¿Puedo usar async/await en funciones flecha?
Sí. Solo tienes que escribir const miFuncion = async () => { ... }. Dentro podrás usar await normalmente.
¿async/await reemplaza completamente a las promesas?
No. La async/await se basa en promesas. Entender cómo funcionan las promesas te ayuda a depurar mejor y a usar correctamente Promise.all, Promise.race y otros métodos avanzados.
¿Qué pasa si rechazo una promesa dentro de una función async sin try/catch?
La función async devuelve una promesa rechazada. Si nadie la consume con catch o await dentro de otro try/catch, se producirá un error de unhandled rejection.
Con estos conceptos ya puedes escribir código asíncrono mucho más legible. Practica combinando async/await con Promise.all y try/catch en proyectos reales. Si quieres seguir aprendiendo más sobre JavaScript, echa un vistazo a más tutoriales de JavaScript o repasa las diferencias entre let, const y var.
Para información oficial sobre promesas puedes consultar la documentación de Promise en MDN. La sintaxis async/await está explicada en detalle en la página de funciones async.
Imagen de portada: Aaaatu, CC BY-SA 4.0, vía Wikimedia Commons (recortada).
