Sintaxis de async/await
Introducción
Tras conocer el concepto de funciones asíncronas, es momento de profundizar en la sintaxis específica de async/await
, la cual ha revolucionado la forma de escribir código asíncrono en JavaScript. Esta combinación de palabras clave proporciona una manera elegante de trabajar con promesas, haciendo que el código asíncrono parezca y se comporte como código síncrono tradicional.
async/await
no es una alternativa a las promesas, sino una capa sintáctica construida sobre ellas que nos permite expresar el mismo comportamiento asíncrono de manera más clara y concisa. En este artículo, exploraremos a fondo la sintaxis de async/await
, sus reglas, patrones y mejores prácticas para incorporarla efectivamente en nuestros proyectos.
La palabra clave await
La palabra clave await
es el corazón de la sintaxis async/await
. Su función principal es pausar la ejecución de una función asíncrona hasta que una promesa se resuelva o se rechace. Cuando utilizamos await
antes de una expresión que devuelve una promesa, la función asíncrona se pausa en esa línea hasta que la promesa se complete.
Sintaxis básica
La sintaxis básica de await
es la siguiente:
// Dentro de una función async
const resultado = await promesa;
Cuando se ejecuta esta línea:
- La ejecución de la función asíncrona se pausa
- JavaScript espera a que la promesa se resuelva o rechace
- Si la promesa se resuelve,
await
devuelve el valor de resolución - Si la promesa se rechaza,
await
lanza una excepción con el valor de rechazo
Veamos un ejemplo práctico:
function obtenerDatos() {
// Esta función devuelve una promesa
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: 1, nombre: "Producto ejemplo" });
}, 2000); // Simulamos un retraso de 2 segundos
});
}
async function mostrarDatos() {
console.log("Solicitando datos...");
// La ejecución se pausa aquí hasta que la promesa se resuelva
const datos = await obtenerDatos();
// Esta línea solo se ejecuta cuando la promesa se ha resuelto
console.log("Datos recibidos:", datos);
return datos; // Esta función devuelve una promesa que se resuelve con datos
}
// Llamamos a la función asíncrona
mostrarDatos()
.then(() => console.log("Proceso completado"))
.catch(error => console.error("Error:", error));
// La consola mostrará:
// "Solicitando datos..."
// (2 segundos después)
// "Datos recibidos: {id: 1, nombre: "Producto ejemplo"}"
// "Proceso completado"
En este ejemplo, cuando llegamos a la línea const datos = await obtenerDatos()
, la función mostrarDatos
se pausa durante 2 segundos mientras espera la resolución de la promesa, y luego continúa con la ejecución del resto del código.
Espera de promesas
Lo que hace que await
sea tan poderoso es que nos permite trabajar con valores asincrónicos como si fueran síncronos. Cuando usamos await
con una promesa, el valor que se devuelve es el valor de resolución de la promesa, no la promesa en sí.
async function ejemplo() {
// Sin await: resultado1 es una promesa
const resultado1 = Promise.resolve(42);
console.log(resultado1); // Promise {<fulfilled>: 42}
// Con await: resultado2 es el valor resuelto (42)
const resultado2 = await Promise.resolve(42);
console.log(resultado2); // 42
}
ejemplo();
Esto nos permite encadenar operaciones asíncronas de forma muy natural:
async function procesoCompleto() {
// Cada paso espera al anterior
const datosUsuario = await obtenerUsuario(123);
const permisos = await obtenerPermisos(datosUsuario.id);
const recursos = await obtenerRecursos(permisos);
return {
usuario: datosUsuario,
permisos: permisos,
recursos: recursos
};
}
Este código es mucho más legible que su equivalente utilizando promesas encadenadas:
function procesoCompletoConPromesas() {
return obtenerUsuario(123)
.then(datosUsuario => {
return obtenerPermisos(datosUsuario.id)
.then(permisos => {
return obtenerRecursos(permisos)
.then(recursos => {
return {
usuario: datosUsuario,
permisos: permisos,
recursos: recursos
};
});
});
});
}
Alternativa al encadenamiento then()
Como vimos en el ejemplo anterior, async/await
ofrece una alternativa más limpia al encadenamiento de métodos .then()
. En lugar de crear una cadena de callbacks anidados, podemos escribir código que sigue un flujo más natural y lineal.
Comparemos un ejemplo más de ambos enfoques:
Con encadenamiento .then():
function cargarYProcesarDatos() {
return cargarDatos()
.then(datos => {
return procesarDatos(datos);
})
.then(datosProcesados => {
return guardarResultados(datosProcesados);
})
.then(informe => {
return enviarNotificacion(informe);
})
.catch(error => {
console.error("Error en el proceso:", error);
return manejarError(error);
});
}
Con async/await:
async function cargarYProcesarDatos() {
try {
const datos = await cargarDatos();
const datosProcesados = await procesarDatos(datos);
const informe = await guardarResultados(datosProcesados);
return await enviarNotificacion(informe);
} catch (error) {
console.error("Error en el proceso:", error);
return manejarError(error);
}
}
Las ventajas de la versión con async/await
incluyen:
- Mayor claridad en el flujo de ejecución
- Menos anidación y complejidad visual
- Manejo de errores más familiar a través de bloques try/catch
- Variables intermedias con mayor alcance (no están limitadas a bloques then)
Legibilidad del código asíncrono
Uno de los mayores beneficios de async/await
es la mejora en la legibilidad del código. Veamos cómo transforma un ejemplo más complejo:
Antes (con promesas):
function obtenerDatosCompletos() {
let datosUsuario;
return autenticar()
.then(token => {
return obtenerPerfil(token);
})
.then(perfil => {
datosUsuario = perfil;
return obtenerAmigos(perfil.id);
})
.then(amigos => {
datosUsuario.amigos = amigos;
const promesasPublicaciones = amigos.map(amigo => {
return obtenerPublicaciones(amigo.id)
.then(publicaciones => {
amigo.publicaciones = publicaciones;
return amigo;
});
});
return Promise.all(promesasPublicaciones);
})
.then(() => {
return obtenerNotificaciones(datosUsuario.id);
})
.then(notificaciones => {
datosUsuario.notificaciones = notificaciones;
return datosUsuario;
})
.catch(error => {
console.error("Error obteniendo datos:", error);
throw new Error("No se pudieron cargar los datos completos");
});
}
Después (con async/await):
async function obtenerDatosCompletos() {
try {
// Autenticación y perfil de usuario
const token = await autenticar();
const datosUsuario = await obtenerPerfil(token);
// Obtenemos amigos
const amigos = await obtenerAmigos(datosUsuario.id);
datosUsuario.amigos = amigos;
// Para cada amigo, obtenemos sus publicaciones
const promesasPublicaciones = amigos.map(async amigo => {
amigo.publicaciones = await obtenerPublicaciones(amigo.id);
return amigo;
});
// Esperamos a que todas las promesas de publicaciones se resuelvan
await Promise.all(promesasPublicaciones);
// Obtenemos notificaciones
datosUsuario.notificaciones = await obtenerNotificaciones(datosUsuario.id);
return datosUsuario;
} catch (error) {
console.error("Error obteniendo datos:", error);
throw new Error("No se pudieron cargar los datos completos");
}
}
La versión con async/await
es considerablemente más fácil de leer y seguir. El flujo de control se asemeja al código síncrono tradicional, lo que facilita entender qué está sucediendo en cada paso.
Limitaciones (solo en funciones async)
Es importante destacar que la palabra clave await
solo puede utilizarse dentro de funciones declaradas con async
. No se puede usar directamente en el ámbito global ni dentro de funciones regulares.
// ❌ Incorrecto: await fuera de una función async
const datos = await obtenerDatos(); // Esto generará un error de sintaxis
// ❌ Incorrecto: await en una función regular
function obtenerDatosAhora() {
const datos = await obtenerDatos(); // Esto también generará un error
return datos;
}
// ✅ Correcto: await dentro de una función async
async function obtenerDatosAhora() {
const datos = await obtenerDatos(); // Esto funciona correctamente
return datos;
}
Esta restricción puede presentar desafíos en ciertos contextos, como en el nivel superior de un script. Para estos casos, podemos usar una IIFE (Expresión de Función Inmediatamente Invocada) asíncrona:
// Función asíncrona auto-ejecutada para código de nivel superior
(async function() {
try {
const datos = await obtenerDatos();
console.log(datos);
} catch (error) {
console.error("Error:", error);
}
})();
En entornos modernos, algunos contextos de JavaScript ahora permiten await
de nivel superior, como en módulos ES o en la consola de desarrollador de los navegadores, pero esto no es universalmente compatible.
Uso con expresiones
await
puede utilizarse con cualquier expresión que devuelva una promesa, no solo con llamadas a funciones. Esto incluye:
- Llamadas a métodos que devuelven promesas:
const respuesta = await fetch('https://api.ejemplo.com/datos');
- Objetos Promise directamente:
const valor = await Promise.resolve(42);
- Expresiones que devuelven promesas:
const primerResultado = await (condicion ? promesa1() : promesa2());
- Constructores de Promise:
const resultado = await new Promise((resolve) => {
setTimeout(() => resolve('completado'), 1000);
});
- Operadores ternarios con promesas:
const datos = await (existeEnCache ? obtenerDeCache() : obtenerDeAPI());
Esta flexibilidad permite integrar await
de manera natural en diferentes patrones de código.
Comportamiento con promesas rechazadas
Cuando utilizamos await
con una promesa que es rechazada, el comportamiento es similar a lanzar una excepción. El valor de rechazo se convierte en una excepción que puede capturarse mediante un bloque try/catch
:
async function ejemplo() {
try {
// Esta promesa será rechazada
const resultado = await Promise.reject(new Error('Algo salió mal'));
// Este código nunca se ejecutará
console.log(resultado);
} catch (error) {
// El error es capturado aquí
console.error('Error capturado:', error.message);
}
}
ejemplo(); // Muestra: "Error capturado: Algo salió mal"
Si no capturamos la excepción con try/catch
, la función asíncrona devolverá una promesa rechazada con el valor de rechazo:
async function ejemploSinCaptura() {
// Esta promesa será rechazada y no la capturamos
const resultado = await Promise.reject(new Error('Sin capturar'));
// Este código nunca se ejecutará
return 'Éxito';
}
ejemploSinCaptura()
.then(valor => console.log(valor))
.catch(error => console.error('Error en promesa:', error.message));
// Muestra: "Error en promesa: Sin capturar"
Manejo de múltiples promesas
Uno de los desafíos con async/await
es cómo manejar múltiples promesas de manera eficiente. Hay varias formas de hacerlo:
1. Secuencial (una después de otra)
async function secuencial() {
const resultado1 = await promesa1();
const resultado2 = await promesa2();
const resultado3 = await promesa3();
return [resultado1, resultado2, resultado3];
}
Esta aproximación ejecuta las promesas una después de otra, lo que puede ser ineficiente si las operaciones son independientes.
2. Paralelo (todas a la vez)
Para ejecutar promesas en paralelo, podemos usar Promise.all()
:
async function paralelo() {
const [resultado1, resultado2, resultado3] = await Promise.all([
promesa1(),
promesa2(),
promesa3()
]);
return [resultado1, resultado2, resultado3];
}
Esta aproximación ejecuta todas las promesas simultáneamente y espera a que todas se completen, lo cual es más eficiente para operaciones independientes.
3. Iniciar promesas antes de await
Otra técnica es iniciar las promesas antes de esperar por ellas:
async function optimizado() {
// Iniciamos todas las promesas inmediatamente
const promesaResultado1 = promesa1();
const promesaResultado2 = promesa2();
const promesaResultado3 = promesa3();
// Ahora esperamos por los resultados
const resultado1 = await promesaResultado1;
const resultado2 = await promesaResultado2;
const resultado3 = await promesaResultado3;
return [resultado1, resultado2, resultado3];
}
Esta técnica inicia todas las operaciones en paralelo pero luego espera por cada resultado en orden, lo que puede ser útil cuando necesitamos resultados en un orden específico.
Mejores prácticas de sintaxis
Para aprovechar al máximo async/await
, aquí hay algunas mejores prácticas de sintaxis:
1. Siempre usa try/catch para manejar errores
async function ejemploRobusto() {
try {
const datos = await operacionAsincrona();
return procesarDatos(datos);
} catch (error) {
console.error("Error en la operación:", error);
// Podemos manejar el error de varias formas:
// 1. Retornar un valor por defecto
return valorPorDefecto;
// 2. Relanzar el error
// throw error;
// 3. Lanzar un error personalizado
// throw new Error(`Error en el procesamiento: ${error.message}`);
}
}
2. Evita mezclar await con .then() o .catch() en el mismo nivel
// ❌ No recomendado: mezcla confusa de paradigmas
async function ejemploConfuso() {
const datos = await operacion1();
return operacion2(datos).then(resultado => {
return resultado.valor;
});
}
// ✅ Recomendado: usa await consistentemente
async function ejemploClaro() {
const datos = await operacion1();
const resultado = await operacion2(datos);
return resultado.valor;
}
3. Usa await en expresiones de retorno cuando sea apropiado
// Versión concisa con await en return
async function ejemploConciso() {
return await operacionFinal();
}
// Si no hay procesamiento adicional, puedes omitir await en el return
async function ejemploDirecto() {
// Esta función también devuelve una promesa
return operacionFinal();
}
Nota: incluir await
en el return
puede ser útil si deseas capturar excepciones dentro de la función actual.
4. Aprovecha la desestructuración con await
async function ejemploDesestructuracion() {
// Desestructuración simple
const { id, nombre } = await obtenerUsuario(123);
// Desestructuración con Promise.all
const [usuario, permisos, preferencias] = await Promise.all([
obtenerUsuario(123),
obtenerPermisos(123),
obtenerPreferencias(123)
]);
return { usuario, permisos, preferencias };
}
5. Usa expresiones IIFE asíncronas para contextos no asíncronos
// Cuando necesites código asíncrono en un contexto síncrono
document.getElementById('boton').addEventListener('click', function() {
// No podemos usar await directamente aquí, así que usamos IIFE
(async function() {
try {
const resultado = await procesarClick();
mostrarResultado(resultado);
} catch (error) {
mostrarError(error);
}
})();
});
6. Evita bloques try/catch anidados excesivos
// ❌ No recomendado: try/catch anidados
async function ejemploAnidacionExcesiva() {
try {
const datosA = await operacionA();
try {
const datosB = await operacionB(datosA);
try {
const datosC = await operacionC(datosB);
return datosC;
} catch (errorC) {
console.error("Error en C:", errorC);
}
} catch (errorB) {
console.error("Error en B:", errorB);
}
} catch (errorA) {
console.error("Error en A:", errorA);
}
}
// ✅ Recomendado: separar en funciones más pequeñas
async function procesarABC() {
try {
const datosA = await operacionA();
const datosB = await operacionB(datosA);
const datosC = await operacionC(datosB);
return datosC;
} catch (error) {
console.error("Error en el proceso:", error);
throw error; // Relanzamos para manejo en nivel superior
}
}
Comportamiento con promesas rechazadas
Cuando usamos await
con una promesa que se rechaza, el comportamiento es similar a lanzar una excepción. Esto significa que podemos usar un bloque try/catch
para manejar el error:
async function ejemploConError() {
try {
const resultado = await funcionQueRechazaPromesa();
// Este código no se ejecutará si la promesa es rechazada
console.log("Resultado:", resultado);
} catch (error) {
// Este código se ejecutará si la promesa es rechazada
console.error("Error capturado:", error);
}
}
Si no capturamos el error con try/catch
, la función asíncrona devolverá una promesa rechazada:
async function sinCapturarError() {
// Si esta promesa se rechaza, la función devolverá una promesa rechazada
const resultado = await funcionQueRechazaPromesa();
return resultado;
}
// Necesitamos manejar el rechazo al llamar a la función
sinCapturarError()
.then(resultado => console.log("Éxito:", resultado))
.catch(error => console.error("Error en la promesa:", error));
Resumen
La sintaxis de async/await
ha transformado la forma en que escribimos código asíncrono en JavaScript, proporcionando una estructura más clara, legible y fácil de mantener. Sus principales características incluyen:
- La palabra clave
await
pausa la ejecución de una función asíncrona hasta que una promesa se resuelve o rechaza - Permite trabajar con valores asincrónicos como si fueran síncronos
- Ofrece una alternativa más limpia al encadenamiento de métodos
.then()
- Mejora significativamente la legibilidad del código asíncrono
- Solo puede utilizarse dentro de funciones declaradas con
async
- Funciona con cualquier expresión que devuelva una promesa
- Transforma los rechazos de promesas en excepciones que pueden capturarse con
try/catch
- Permite manejar múltiples promesas de manera secuencial o paralela
Al dominar la sintaxis de async/await
, podemos escribir código asíncrono que es más intuitivo, más fácil de depurar y más resistente a errores. Esta combinación de palabras clave es una de las mejoras más significativas en JavaScript moderno y una herramienta esencial en el arsenal de cualquier desarrollador.