TL;DR:
Usa return await en funciones async para obtener mejores stack traces durante la depuración; su impacto en el rendimiento es insignificante en la mayoría de los casos.
La programación asíncrona puede ser complicada, especialmente cuando se trabaja con funciones async
y el uso de await
, return await
y return
.
Hoy profundizaremos en estos conceptos, entenderemos sus diferencias y aprenderemos cuándo usar cada uno de ellos.
Introducción a las funciones asíncronas
Una breve introducción para asegurarnos de que todos estamos en la misma página:
JavaScript es un lenguaje de un solo hilo, lo que significa que puede ejecutar una tarea a la vez. Sin embargo, las aplicaciones modernas a menudo necesitan realizar operaciones que consumen tiempo, como obtener datos de un servidor o leer archivos. Para evitar bloquear el hilo principal y mantener la aplicación receptiva, JavaScript utiliza la programación asíncrona.
Las funciones asíncronas te permiten escribir código que no espera a que una tarea se complete antes de pasar a la siguiente.
Antes de async
y await
, usábamos callbacks y Promises para manejar operaciones asíncronas. Aunque funcionales, estos métodos podían llevar a estructuras de código complejas y difíciles de leer, a menudo referidas como "callback hell".
// Callback Hell
fetchData(function (error, data) {
if (error) {
handleError(error);
} else {
processData(data, function (error, processedData) {
if (error) {
handleError(error);
} else {
// Further processing or final actions
}
});
}
});
// Using Promises
fetchData()
.then(data => processData(data))
.catch(error => handleError(error))
Con la introducción de async
y await
, JavaScript proporciona una forma más sencilla y legible de manejar código asíncrono, haciéndolo parecer y comportarse más como código síncrono.
Entendiendo async
y await
La palabra clave async
- Propósito: Declara una función como asíncrona.
- Comportamiento: Toda función
async
devuelve una Promise. Si la función devuelve un valor, la Promise resolverá ese valor. Si la función lanza un error —esto es crucial— la Promise se rechaza con ese error. Hablaremos más sobre el manejo de errores más adelante.
async function fetchData() {
return 'Data fetched';
}
fetchData().then((data) => console.log(data)); // Outputs: Data fetched
async function fetchData() {
throw Error('Error while fetching');
}
fetchData().catch((error) => console.log(error)); // Outputs: Error: 'Error while fetching'
La palabra clave await
- Propósito: Pausa la ejecución de una función
async
hasta que una Promise devuelve un resultado (resolve o reject). - Comportamiento: Solo se puede usar dentro de funciones
async
. Hace que el código espere a que la Promise se resuelva y devuelve el valor resuelto.
...
async function getData() {
const data = await fetchData();
console.log(data);
}
getData(); // Outputs: Data fetched
Al usar await
, puedes escribir código asíncrono que se lee como código síncrono, lo que facilita su comprensión y mantenimiento.
Una pregunta antes de continuar: ¿qué sucederá si fetchData retorna un error?.
await
y try/catch
La palabra clave await
juega un papel fundamental si deseas manejar errores en tus funciones async
.
Si usas try/catch
dentro de una función async
y await
en tus resultados, cualquier rechazo de Promise se manejará de manera adecuada, transformándose en una excepción.
const delayAndFail = (time = 2000) =>
new Promise((resolve, reject) =>
setTimeout(() => {
reject('Something went wrong!');
}, time),
);
async function execute() {
try {
await delayAndFail();
} catch (error) {
console.log(error);
}
}
execute(); // Outputs: 'Something went wrong!'
Pero, si no haces await
a tus resultados dentro de un bloque try/catch
, el rechazo de la Promise no será capturado.
...
async function execute() {
try {
delayAndFail(); // No awaiting here
} catch (error) {
console.log(error);
}
}
execute(); // Outputs: 'Uncaught (in promise) Something went wrong!'
Diferencias entre await
, return await
y return
Una de las partes más complicadas de entender (al menos para mí) es cuándo usar estas nuevas palabras clave, específicamente return
y return await
.
Hay muchos artículos que sugieren diferentes enfoques. También existen algunas reglas generales, que al final fueron descartadas…
Vamos a revisarlo de una vez por todas, porque entender las diferencias es crucial.
await
- Uso: Se utiliza dentro de una función
async
para esperar a que una Promise se resuelva antes de proceder a la siguiente instrucción. - Efecto: Pausa la ejecución de la función hasta que la Promise esperada se resuelve, luego devuelve el valor resuelto.
async function execute() {
await delayAndFail();
}
Esto debería estar bastante claro a este punto. Pero ahora, return
se une a la fiesta:
return await
- Uso: Espera a que la Promise se resuelva y luego devuelve el valor resuelto.
- Efecto: Similar a usar
await
y luego devolver el valor.
async function execute() {
return await delayAndFail();
}
return
- Uso: Devuelve una Promise inmediatamente, sin esperar a que se resuelva dentro de la función.
- Efecto: La función sale y devuelve una Promise pendiente. El que llama a la función puede manejar la Promise.
Ejemplo:
async function execute() {
return delayAndFail();
}
Por qué la diferencia importa
Como puedes ver, hacer await
antes de return
puede parecer redundante. Es más o menos lo mismo, ¿verdad?.
No, no lo es. Hay una razón por la que casi siempre deberías usar return await
en lugar de solo return
: para obtener mejor información de errores al manejar rechazos de Promises.
Cuando añadimos await
después de return
, estamos (prácticamente hablando) esperando dentro de la función a que la Promise se cumpla (resuelva o rechace) antes de hacer cualquier cosa. Si la Promise se rechaza, entonces una referencia a la función estará presente en el stack trace.
async function fail() {
try {
await Promise.reject("I'm failing")
} catch(error) {
throw new Error(error.message)
}
}
async function execute() {
return await fail();
}
execute().catch(error => console.log(error.stack));
/* Outputs this trace
Error
at fail (<anonymous>:5:11)
at async execute (<anonymous>:10:10) */
Pero si dejamos de hacer await
antes del return
, no hay rastro de la función execute
.
...
async function execute() {
return fail();
}
execute().catch(error => console.log(error.stack));
/* Outputs this trace
Error
at fail (<anonymous>:5:11) */
Por cierto, todo esto lo trato más a fondo (con ejemplos reales) en el taller de JavaScript Moderno.
Pero es menos eficiente
Sí, usar return
sin await
puede ser ligeramente más eficiente porque no añade una microtarea extra al event loop. En mi experiencia todo lo que puedo decir es esto: para la mayoría de los escenarios prácticos, esta supuesta penalización de rendimiento ni siquiera importa. De hecho, parece ser que podrías estar haciéndote más daño que bien.
Conclusión
Usar await
permite que tu código asíncrono se lea y se comporte como código síncrono, haciéndolo más intuitivo.
Cuando se trata de elegir entre return
y return await
, recuerda que return await
puede proporcionar stack traces más completos, lo cual es vital para depurar. Aunque hay una pequeña penalización en rendimiento, en la mayoría de las aplicaciones del mundo real, la claridad y el mejor manejo de errores superan con creces su impacto.