Async/Await en javaScript

3
84218

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

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 incluye undefined 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:

3 COMENTARIOS

  1. 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!!

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad