En este tutorial veremos qué es la especificación async/await, como transpilar nuestro código para poder usarlo en navegadores y qué ventajas ofrece
respecto a las promesas.
Índice de contenidos
- 1. Introducción
- 2. Entorno
- 3. Limitaciones de las promesas
- 4. Async/Await y promesas
- 5. Limitaciones de Async/Await
- 6. Transpilando async/await
- 7. Conclusiones
- 8. Referencias
1. Introducción
Las promesas en javascript son una manera de conseguir que el código asíncrono se comporte como si fuera síncrono, facilitándonos la vida enormemente. Sin embargo, esto tiene algunas limitaciones.
Async/Await
es una propuesta para extender la sintaxis de javaScript con las palabras reservadas async
y await
, cuyo uso permitirá -cuando se estandarice- tratar
las funciones que devuelven promesas en nuestro código como si fueran funciones síncronas que devuelven directamente valores en vez de promesas.
Aunque actualmente ningún navegador soporta async/await
, podemos utilizar este mecanismo transpilando código que lo utilice a código que sí entiendan los navegadores.
Cómo funciona:
Por ejemplo, sea getFilm
una función que se conecta a un hipotético servicio web que devuelve una película al azar, y getMain()
otra función promisificada del mismo servicio que devuelve el nombre del protagonista de una película dada. Con la sintaxis de Async/Await
podemos hacer lo siguiente:
async function queue(){
var film = await getFilm(); //Supongamos que toca 'Matrix'
var main = await getMain(film); //Neo
console.log(main);
}
queue();//escribirá 'Neo' en la consola.
En vez de tener que utilizar la respuesta de la primera petición en un incómodo then( function(){} )
, usando la palabra especial await
podemos usar getFilm()
y getMain()
como si devolvieran valores síncronos en vez de promesas.
Esto es mucho más cómodo de escribir, y sobre todo resulta en código mucho más legible.
2. Entorno
Cualquier editor de texto y un navegador debería valer para ejecutar snippets de javaScript.
No obstante, async/await
es una especificación de ecmaScript 7, y todavía no es soportada de forma nativa por ningún navegador ni por node.js. Para poder utilizar async/await en nuestro código a día de hoy, tendremos que usar algún transpilador para convertir nuestro código con la sintaxis de async/await
en código que utilice promesas o generadores.
Como es un proceso relativamente complejo, he dedicado más abajo un capítulo a este tema.
3. Limitaciones de las promesas
El código que pusimos más arriba, asumiendo como hemos dicho que getFilm
y getMain
son métodos que hacen peticiones http a un servicio web y devuelven promesas, sería equivalente al siguiente:
getFilm()
.then(getMain)
.then(console.log);
Que, tal vez, sabiendo cómo funcionan las promesas, sea más fácil de escribir y de leer.
No obstante, esto es porque getMain
y console.log
toman un solo parámetro, lo que hace que sea muy sencillo pasarlas directamente a then
como parámetro.
Imaginemos un cambio tan tonto como hacer un console.log al final del protagonista y también del film. El código promisificado se vuelve como el que sigue:
var film = null;
getFilm( function(res){
film = res;
return getMain(film);
})
.then(function(res){
console.log(film , res);
});
De pronto, es bastante más complejo y enrevesado. En cambio, si queremos retocar nuestro anterior código con async/await
, podríamos simplemente hacer:
(async function(){
var film = await getFilm();
var main = await getMain(film);
console.log(main , film);
})();
A parte de la molestia de tener que envolver todo el código en una función declarada con la palabra reservada await
-en este caso es una función auto-invocada, IIFE-, y de tener que usar await
antes de invocar funciones que devuelven promesas, realizar cambios en código que utiliza async/await es mucho más sencillo que en código promisificado, y es mucho más agradable de leer.
4. Async/Await
Cuando usamos la palabra reservada async
al declarar una función, suceden dos cosas:
- Podemos usar la palabra
await
dentro de esa función para acceder directamente a los valores que devolverían métodos que devuelven promesas. - La propia función que estamos declarando devuelve su valor de retorno como una promesa.
Cuando usamos la palabra reservada await
al invocar una función:
- Si la función sin
await
hubiera devuelto una promesa satisfecha, la llamada devolverá el valor de esa promesa. - Si la función sin
await
hubiera devuelto una promesa rechazada, lanzará un error con la razón del rechazo. - Si la función sin
await
hubiera devuelto un valor que no es una promesa, la llamada devolverá ese mismo valor (esto incluyeundefined
en llamadas a funciones sin valor de retorno).
Intentar utilizar await
en cualquier lugar que no sea una función declarada como async
resultará en un error. Por el contrario, se puede utilizar la palabra reservada async
para declarar funciones sin usar await
en ningún momento; no es que sea muy útil, sin embargo.
Funciones async como promesas
Como dijimos, cuando declaramos una función usando async
podemos encadenar then
y catch
tras ella, dado que es una promesa:
async function get(){
return 100;
}
film().then(console.log); /100
Esto es así aunque internamente no haya código asíncrono de ninguna clase (como en el ejemplo, donde get()
devuelve ‘100’ sin más).
Esto tiene sentido, dado que async
está diseñada para utilizar await
dentro de ella. Como los valores que devuelve await
siempre son valores definitivos y no promesas, pero realmente el código es asíncrono, es necesario que las funciones declaradas como async
, por su parte, devuelvan promesas.
Gestión de errores
La función get(), al estar declarada con la palabra aysnc
, automáticamente devuelve su valor como una promesa. Incluso si, como en este caso, devolvía directamente un valor sin ninguna llamada ajax ni nada que tenga que ver con el código típicamente asíncrono.
Esto funciona igualmente cuando hay errores. Si hay algún error dentro de una función declarada como async
, éste es automáticamente atrapado y devuelto como una promesa rechazada:
async function throwError(){
throw new Error('Esto no aparecerá como un Error sino como una promesa rechazada');
}
throwError().catch(console.log); //Error: Esto no aparecerá como un error sino como una promesa rechazada...
No solamente los errores se convierten automáticamente en promesas cuando una función declarada con async
es llamada:
Además, y de forma inversa, cuando una llamada precedida por await
devuelve una promesa rechazada, ésta se convierte en un error:
function rejectedPromise(){
return new Promise( function(resolve , reject){
reject('promesa rechazada');
});
/*Es mala práctica rechazar promesas con strings. lo adecuado sería:
reject(new Error('promesa rechazada') );
En este caso, evito instanciar un Error aquí para que quede claro que es
async/await quien está generando y atrapando por su cuenta
un Error en la llamada a get()*/
}
async function get(){
try{
await rejectedPromise();
}
catch(error){
console.log(error);
}
}
get();//Promesa rechazada
Realmente, no hay mucho más que decir sobre las funcionalidades async/await
. Una vez que se entiende cómo async
y await
se relacionan con las promesas, es realmente sencillo y cómodo utilizar esta sintaxis.
Limitaciones de async/await
Errores
Como el código que generamos para poder utilizar -a día de hoy- async/await
es transpilado, el código real interpretado por los navegadores no es el código que hemos escrito, sino el código transpilado. Aunque podemos acceder en el navegador a nuestro propio código original por medio de sourcemaps, podemos tener bastantes problemas con esto:
- No siempre el código transpilado funcionará bien, o como creemos que funciona. Este tipo de bugs son muy difíciles de entender y detectar.
- A veces, el navegador puede volverse loco y no traducir bien las líneas entre los scripts al añadir breakpoints. Suelen ser problemas de caché, pero pueden volverte loco hasta que entiendas lo que está pasando.
Al margen del problema de la transpilación, que no durará para siempre (en algún momento todos los navegadores soportarán async/await
nativamente), await
hace referencia siempre a promesas. Cuando encontremos errores, serán seguramente promesas que han sido rechazadas -pero invocadas con await
-, fruto de algún Error
real dentro de algún callback. Y al revés, cuando encontremos promesas rechazadas puede que originalmente fueran errores que han sido transformados en promesas por async
.
Es importante tener muy presente cómo async
y await
modifican promesas y errores para poder entender dónde exactamente ha habido un fallo cuando estemos depurando código con async/await
, o podemos volvernos más locos que con un callback hell.
Scope
Async/await
permite que nuestro código asíncrono se comporte como si fuera síncrono. Pero esto está limitado únicamente al scope
de funciones declaradas mediante async
.
Cualquier línea de código ejecutada inmediatamente después de una llamada a una función async
no esperará a ninguna de las llamadas internas de esa función que utilicen await
. Un ejemplo:
async function getMain(){
var film = await getFilm();
return await getMain(film);
};
getMain().then(console.log)
console.log('fin del script'); //esta línea se ejecutará antes que el log anterior.
Aquí, la segunda línea dentro de getMain
espera a la primera -aunque sea una llamada asíncrona- por el uso de los await
dentro de la función getMain
, por estar declarada mediante async
.
No obstante, los logs posteriores no están dentro de una función async
. El then
proveniente de getMain()
se ejecutará después del log que imprime ‘fin del script’, porque la llamada es asíncrona.
Las alternativas a esto, para que el snippet se comporte como queremos, sería encadenar el último log a la promesa que devuelve getMain
:
getMain(console.log).then( function(){ console.log('fin del script') });
O, por qué no, lanzar los logs a su vez dentro de una función async
:
(async function(){
console.log( await getMain() );
console.log('fin del script'); //Ahora sí que aparecerá al final
})();
Por cierto, como se ve en este ejemplo, dado que getMain
, por ser async
, devuelve una promesa, puede a su vez ser invocada mediante await
en otra función async
.
A parte de todo esto, la limitación más obvia es el uso necesario de las propias palabras reservadas async
y await
, pero es un mal menor si tenemos en cuenta que las alternativas implican una sintaxis mucho más fea y complicada.
Transpilando async/await
Como hemos dicho más arriba, la especificación async/await no está soportada por ningún navegador. Si utilizas las palabras reservadas async
o await
en algún navegador actual, éste lanzará un error quejándose de que async
y await
son palabras reservadas.
Para poder usar esta especificación en código real, debemos usar un transpilador que lea nuestro código que utiliza async/await
y lo transforme en código que utilice promesas.
Para ello utilizaremos el transpilador Babel y su plugin transform async to generator
.
He creado un repositorio en github a modo de ejemplo de cómo usar webpack y babel para transpilar código que contenga async/await
a código legible por navegadores.
Podéis encontrar el repositorio en este enlace.
La clave para que todo funcione es añadir el preset es-2017 en el fichero de configuración de babel y en el de webpack, y no olvidarse de incluír el plugin transform-async-to-generator
en la configuración de babel, así cómo el resto de dependencias que webpack y babel necesitan para poder transpilar el código. Podeís ver la lista de plugins necesarios en el package.json
.
7. Conclusiones
Por el precio de incluir en nuestro código las palabras especiales async
y await
, podemos trabajar con código asíncrono de forma mucho más cómoda que utilizando promesas o callbacks. Realmente, una vez transpilado el código, todo son ventajas.
El problema es que es mandatorio transpilarlo para poder hacer algo con nuestro código. Si estás trabajando en un proyecto donde ya se utiliza algún transpilador para utilizar ES6 o alguna otra feature experimental de ES7, añadir async/await
no supondrá mucho problema y hacerlo sería altamente recomendable.
Sin embargo, si ese no es el caso, puede que no quieras empezar a transpilar tu código solamente por la posibilidad de usar async/await
. Lo cierto es que babel
es bastante lento y transpilar varios ficheros de una tacada puede llevar varios segundos en una base de código extensa.
Aunque usar transpiladores está muy ‘a la moda’, la verdad es que son lentos y esto puede afectar bastante a la mecánica de desarrollo, y también es tiempo que hay que sumar a cada build.
Así que, si bien async/await
es una mecánica genial, no todos los proyectos pueden incorporarlo sin contrapartidas, y algunos tendrán que esperar a que los navegadores lo soporten.
Por último, que las funciones declaradas con async
se transformen automáticamente en promesas, tiene el efecto de que es muy sencillo añadir incrementalmente async/await
a una base de código que utiliza promesas.
Sin embargo, esto no es cierto si nuestro código está plagado de callbacks y queremos empezar a introducir async/await
poco a poco. Si redefinimos una función que ejecuta un callback para que devuelva una promesa, todos sus clientes deberán actualizarse.
8. Referencias
Algunos de los mejores enlaces que podéis leer sobre async/await
en JavaScript son los siguientes:
Así será más fácil huir del siempre temido callback hell.
Eres un figura. Muchas gracias Saludos
Excelente artículo Mauro!! Mis felicitaciones y agradecimientos por tu trabajo. Me fue de gran utilidad para comprender el tema.
Una observación: Creo que en el primer ejemplo del punto 4, la función a llamar es get() y no film().
Un fuerte abrazo desde Argentina!!