Node.js Async Await en ES7

Una de las características más interesantes de JavaScript (y, por lo tanto, de Node.js) es la sintaxis async/await que se está introduciendo en ES7. Aunque es básicamente solo s...

Una de las funciones más interesantes de JavaScript (y, por lo tanto, de Node.js) es la sintaxis async/await que se introduce en ES7. Aunque básicamente es azúcar sintáctico además de Promises, estas dos palabras clave por sí solas deberían hacer que escribir código asíncrono en Node sea mucho más soportable. Casi elimina el problema de infierno de devolución de llamada, e incluso usemos control- estructuras de flujo alrededor de nuestro código asíncrono.

A lo largo de este artículo, veremos qué es lo que está mal con Promises, cómo puede ayudar la nueva función await y cómo puede comenzar a usarla ahora mismo.

El problema de las promesas {#el problema de las promesas}

El concepto de "promesa" en JavaScript existe desde hace un tiempo y se puede utilizar desde hace años gracias a bibliotecas de terceros como [Azulejo](https://www.npmjs.com/package/ bluebird) y q, sin mencionar el soporte nativo agregado recientemente en ES6.

Han sido una gran solución al problema del infierno de devolución de llamada, pero desafortunadamente no resuelven todos los problemas asincrónicos. Si bien es una gran mejora, Promises nos deja con ganas de una simplificación aún mayor.

Digamos que quieres usar la API REST de Github para encontrar la cantidad de estrellas que tiene un proyecto. En este caso, probablemente usaría la gran biblioteca petición-promesa. Usando el enfoque basado en Promise, debe realizar la solicitud y obtener el resultado dentro de la devolución de llamada que pasa a .then(), así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var request = require('request-promise');

var options = {
    url: 'https://api.github.com/repos/scottwrobinson/camo',
    headers: {
        'User-Agent': 'YOUR-GITHUB-USERNAME'
    }
};

request.get(options).then(function(body) {
    var json = JSON.parse(body);
    console.log('Camo has', json.stargazers_count, 'stars!');
});

Esto imprimirá algo como:

1
2
$ node index.js
Camo has 1,000,000 stars!

De acuerdo, tal vez ese número sea una ligera exageración, pero entiendes el punto;)

Hacer solo una solicitud como esta no es demasiado difícil con Promises, pero ¿qué pasa si queremos hacer la misma solicitud para muchos repositorios diferentes en GitHub? ¿Y qué sucede si necesitamos agregar un flujo de control (como condicionales o bucles) alrededor de las solicitudes? A medida que sus requisitos se vuelven más complicados, las promesas se vuelven más difíciles de trabajar y aún terminan complicando su código. Todavía son mejores que las devoluciones de llamadas normales, ya que no tiene un anidamiento ilimitado, pero no resuelven todos sus problemas.

Para escenarios más complicados como el del siguiente código, debe volverse bueno en encadenar Promesas y comprender cuándo y dónde se ejecuta su código asíncrono.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
"use strict";

var request = require('request-promise');

var headers = {
    'User-Agent': 'YOUR-GITHUB-USERNAME'
};

var repos = [
    'scottwrobinson/camo',
    'facebook/react',
    'scottwrobinson/twentyjs',
    'moment/moment',
    'nodejs/node',
    'lodash/lodash'
];

var issueTitles = [];

var reqs = Promise.resolve();

repos.forEach(function(r) {
    var options = { url: 'https://api.github.com/repos/' + r, headers: headers };

    reqs = reqs.then(function() {
        return request.get(options);
    }).then(function(body) {
        var json = JSON.parse(body);

        var p = Promise.resolve();

        // Only make request if it has open issues
        if (json.has_issues) {
            var issuesOptions = { url: 'https://api.github.com/repos/' + r + '/issues', headers: headers };
            p = request.get(issuesOptions).then(function(ibody) {
                var issuesJson = JSON.parse(ibody);

                if (issuesJson[0]) {
                    issueTitles.push(issuesJson[0].title);
                }
            });
        }

        return p;
    });
});

reqs.then(function() {
    console.log('Issue titles:');
    issueTitles.forEach(function(t) {
        console.log(t);
    });
});

Nota: Github limita agresivamente la velocidad de las solicitudes no autenticadas, así que no se sorprenda si se corta después de ejecutar el código anterior solo unas pocas veces. Puede aumentar este límite pasar un ID/secreto de cliente.

En el momento de escribir este artículo, la ejecución de este código produciría lo siguiente:

1
2
3
4
5
6
$ node index.js
Issue titles:
feature request: bulk create/save support
Made renderIntoDocument tests asynchronous.
moment issue template
test: robust handling of env for npm-test-install

Simplemente agregando un bucle for y una declaración if a nuestro código asincrónico hace que sea mucho más difícil de leer y comprender. Este tipo de complejidad solo se puede mantener durante tanto tiempo antes de que se vuelva demasiado difícil trabajar con ella.

Mirando el código, ¿puede decirme de inmediato dónde se ejecutan realmente las solicitudes o en qué orden se ejecuta cada bloque de código? Probablemente no sin leerlo detenidamente.

Simplificando con Async/Await

La nueva sintaxis async/await le permite seguir usando Promises, pero elimina la necesidad de proporcionar una devolución de llamada a los métodos then() encadenados. El valor que se habría enviado a la devolución de llamada then() se devuelve directamente desde la función asíncrona, como si fuera una función de bloqueo síncrona.

1
let value = await myPromisifiedFunction();

Aunque aparentemente simple, esta es una gran simplificación para el diseño de código JavaScript asíncrono. La única sintaxis adicional necesaria para lograr esto es la palabra clave await. Entonces, si comprende cómo funcionan las Promesas, entonces no será demasiado difícil entender cómo usar estas nuevas palabras clave, ya que se basan en el concepto de Promesas. Todo lo que realmente tienes que saber es que cualquier Promesa puede ser en espera-ed. Los valores también se pueden esperar, al igual que una Promesa puede .resolve() en un entero o una cadena.

Comparemos el método basado en Promise con la palabra clave await:

Promesas

1
2
3
4
5
var request = require('request-promise');

request.get('https://api.github.com/repos/scottwrobinson/camo').then(function(body) {
    console.log('Body:', body);
});

esperar

1
2
3
4
5
6
7
var request = require('request-promise');

async function main() {
    var body = await request.get('https://api.github.com/repos/scottwrobinson/camo');
    console.log('Body:', body);
}
main();

Como puede ver, await indica que desea resolver la Promesa y no devolver ese objeto Promesa real como lo haría normalmente. Cuando se ejecuta esta línea, la llamada request se colocará en la pila del bucle de eventos y la ejecución dará lugar a otro código asíncrono que está listo para ser procesado.

La palabra clave async se utiliza cuando se define una función que contiene código asíncrono. Este es un indicador de que la función devuelve una promesa y, por lo tanto, debe tratarse como asíncrona.

Aquí hay un ejemplo simple de su uso (observe el cambio en la definición de la función):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
async function getCamoJson() {
    var options = {
        url: 'https://api.github.com/repos/scottwrobinson/camo',
        headers: {
            'User-Agent': 'YOUR-GITHUB-USERNAME'
        }
    };
    return await request.get(options);
}

var body = await getCamoJson();

Ahora que sabemos cómo usar async y await juntos, veamos cómo se ve ahora el código más complejo basado en Promise de antes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
"use strict";

var request = require('request-promise');

var headers = {
    'User-Agent': 'scottwrobinson'
};

var repos = [
    'scottwrobinson/camo',
    'facebook/react',
    'scottwrobinson/twentyjs',
    'moment/moment',
    'nodejs/node',
    'lodash/lodash'
];

var issueTitles = [];

async function main() {
    for (let i = 0; i < repos.length; i++) {
        let options = { url: 'https://api.github.com/repos/' + repos[i], headers: headers };
        let body = await request.get(options);
        let json = JSON.parse(body);

        if (json.has_issues) {
            let issuesOptions = { url: 'https://api.github.com/repos/' + repos[i] + '/issues', headers: headers };
            let ibody = await request.get(issuesOptions);
            let issuesJson = JSON.parse(ibody);

            if (issuesJson[0]) {
                issueTitles.push(issuesJson[0].title);
            }
        }
    }

    console.log('Issue titles:');
    issueTitles.forEach(function(t) {
        console.log(t);
    });
}

main();

Sin duda, es más legible ahora que se puede escribir como muchos otros lenguajes ejecutados linealmente.

Ahora, el único problema es que cada llamada request.get() se ejecuta en serie (lo que significa que cada llamada tiene que esperar hasta que la llamada anterior haya terminado antes de ejecutarse), por lo que tenemos que esperar más tiempo para que el código complete la ejecución antes de obtener nuestros resultados. La mejor opción sería ejecutar las solicitudes HTTP GET en paralelo. Esto todavía se puede hacer utilizando Promise.all() como lo hubiéramos hecho antes. Simplemente reemplace el ciclo for con una llamada .map() y envíe la matriz resultante de Promises a Promise.all(), así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Init code omitted...

async function main() {
    let reqs = repos.map(async function(r) {
        let options = { url: 'https://api.github.com/repos/' + r, headers: headers };
        let body = await request.get(options);
        let json = JSON.parse(body);

        if (json.has_issues) {
            let issuesOptions = { url: 'https://api.github.com/repos/' + r + '/issues', headers: headers };
            let ibody = await request.get(issuesOptions);
            let issuesJson = JSON.parse(ibody);

            if (issuesJson[0]) {
                issueTitles.push(issuesJson[0].title);
            }
        }
    });

    await Promise.all(reqs);
}

main();

De esta manera puedes aprovechar la velocidad de la ejecución en paralelo y la simplicidad de await.

Hay más beneficios que solo poder usar el flujo de control tradicional como bucles y condicionales. Este enfoque lineal nos permite volver a usar la sentencia try...catch para manejar errores. Con Promises, tenía que usar el método .catch(), que funcionó, pero podría causar confusión al determinar para qué Promises detectó excepciones.

Así que ahora esto...

1
2
3
4
5
6
7
request.get('https://api.github.com/repos/scottwrobinson/camo').then(function(body) {
    console.log(body);
}).catch(function(err) {
    console.log('Got an error:', err.message);
});

// Got an error: 403 - "Request forbidden by administrative rules. Please make sure your request has a User-Agent header..."

...puede expresarse así:

1
2
3
4
5
6
7
8
try {
    var body = await request.get('https://api.github.com/repos/scottwrobinson/camo');
    console.log(body);
} catch(err) {
    console.log('Got an error:', err.message)
}

// Got an error: 403 - "Request forbidden by administrative rules. Please make sure your request has a User-Agent header..."

Si bien se trata de la misma cantidad de código, es mucho más fácil de leer y comprender para alguien que hace la transición a JavaScript desde otro idioma.

Uso de Async ahora mismo

La función asíncrona todavía está en la etapa de propuesta, pero no se preocupe, todavía hay algunas formas en que puede usar esto en su código ahora mismo.

V8

Si bien aún no ha llegado a Node, el equipo de V8 tiene declaró públicamente su intención de implementar la característica async/await. Incluso ya han comprometido la implementación del tiempo de ejecución del prototipo, lo que significa que el soporte de armonía no debería estar muy atrás.

Babel

Podría decirse que la opción más popular es transpilar su código usando Babel y sus diversos complementos. Babel es extremadamente popular gracias a su capacidad para mezclar y combinar funciones de ES6 y ES7 mediante su sistema de complementos. Si bien es un poco más complicado de configurar, también brinda mucho más control al desarrollador.

Regenerador

El proyecto regenerador de Facebook no tiene tantas funciones como Babel, pero es una forma más sencilla de hacer que funcione la transpilación asíncrona.

El mayor problema que he tenido es que sus errores no son muy descriptivos. Entonces, si hay un error de sintaxis en su código, no obtendrá mucha ayuda del regenerador para encontrarlo. Aparte de eso, he estado feliz con eso.

Trazador

Personalmente, no tengo experiencia con este, pero rastreador (de Google) parece ser otra opción popular con muchas funciones disponibles. Puede encontrar más información aquí para obtener detalles sobre qué funciones de ES6 y ES7 se pueden transpilar.

asincaespera

La mayoría de las opciones disponibles implican la transpilación o el uso de una compilación nocturna de V8 para que funcione “async”. Otra opción es usar el paquete asincaesperar, que proporciona una función para resolver Promises de manera similar a la función await. Es una buena forma estándar de ES5 de obtener una sintaxis similar.

Conclusión

¡Y eso es! Personalmente, estoy muy entusiasmado con esta función en ES7, pero hay algunas otras excelentes características en ES7 que debería consultar, como decoradores de clase y propiedades.

¿Utiliza el código ES7 transpilado? Si es así, ¿qué característica ha sido la más beneficiosa para su trabajo? ¡Cuéntanos en los comentarios!