Conversión de devoluciones de llamada a promesas en Node.js

JavaScript asíncrono usó mucho las devoluciones de llamada, pero ahora usa Promises porque es más fácil de administrar. En este artículo, convertiremos las devoluciones de llamadas en promesas.

Introducción

Hace unos años, las devoluciones de llamada eran la única forma en que podíamos lograr la ejecución de código asíncrono en JavaScript. Hubo algunos problemas con las devoluciones de llamada y el más notable fue "Infierno de devolución de llamada".

Con ES6, se introdujeron Promises como una solución a esos problemas. Y finalmente, se introdujeron las palabras clave async/await para una experiencia aún más placentera y una mejor legibilidad.

Incluso con la adición de nuevos enfoques, todavía hay muchos módulos y bibliotecas nativos que usan devoluciones de llamada. En este artículo, vamos a hablar sobre cómo convertir las devoluciones de llamada de JavaScript a Promises. El conocimiento de ES6 será útil ya que usaremos funciones como operadores de propagación para facilitar las cosas.

¿Qué es una devolución de llamada

Una devolución de llamada es un argumento de función que resulta ser una función en sí misma. Si bien podemos crear cualquier función para aceptar otra función, las devoluciones de llamada se usan principalmente en operaciones asincrónicas.

JavaScript es un lenguaje interpretado que solo puede procesar una línea de código a la vez. Algunas tareas pueden tardar mucho en completarse, como descargar o leer un archivo grande. JavaScript descarga estas tareas de ejecución prolongada a un proceso diferente en el navegador o en el entorno de Node.js. De esa manera, no bloquea la ejecución del resto del código.

Por lo general, las funciones asincrónicas aceptan una función de devolución de llamada, de modo que cuando estén completas podamos procesar sus datos.

Tomemos un ejemplo, escribiremos una función de devolución de llamada que se ejecutará cuando el programa lea con éxito un archivo de nuestro disco duro.

Para ello, utilizaremos un archivo de texto llamado sample.txt, que contiene lo siguiente:

1
Hello world from sample.txt

Then let's write a simple Node.js script to leer el archivo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const fs = require('fs');

fs.readFile('./sample.txt', 'utf-8', (err, data) => {
    if (err) {
        // Handle error
        console.error(err);
          return;
    }

    // Data is string do something with it
    console.log(data);
});

for (let i = 0; i < 10; i++) {
    console.log(i);
}

Ejecutar este código debería producir:

1
2
3
4
5
0
...
8
9
Hello world from sample.txt

Si ejecuta este código, debería ver que se imprime 0..9 antes de que se ejecute la devolución de llamada. Esto se debe a la gestión asíncrona de JavaScript de la que hemos hablado anteriormente. La devolución de llamada, que registra el contenido del archivo, solo se llamará después de leer el archivo.

Como nota al margen, las devoluciones de llamada también se pueden usar en métodos sincrónicos. Por ejemplo, Array.sort() acepta una función de devolución de llamada que le permite personalizar cómo se ordenan los elementos.

Las funciones que aceptan devoluciones de llamada se denominan funciones de orden superior.

Ahora tenemos una mejor idea de las devoluciones de llamada. Avancemos y veamos qué es una Promesa.

¿Qué es una promesa

Se introdujeron promesas con ECMAScript 2015 (comúnmente conocido como ES6) para mejorar la experiencia del desarrollador con la programación asíncrona. Como sugiere su nombre, es una promesa de que un objeto JavaScript eventualmente devolverá un valor o un error.

Una promesa tiene 3 estados:

  • Pendiente: el estado inicial que indica que la operación asíncrona no está completa.
  • Cumplido: lo que significa que la operación asíncrona se completó correctamente.
  • Rechazado: lo que significa que la operación asíncrona falló.

La mayoría de las promesas terminan luciendo así:

1
2
3
4
5
6
7
8
9
someAsynchronousFunction()
    .then(data => {
        // After promise is fulfilled
        console.log(data);
    })
    .catch(err => {
        // If promise is rejected
        console.error(err);
    });

Las promesas son importantes en JavaScript moderno, ya que se usan con las palabras clave async/await que se introdujeron en ECMAScript 2016. Con async/await, no necesitamos usar devoluciones de llamada o then() y catch() para escribir código asíncrono.

Si se adaptara el ejemplo anterior, quedaría así:

1
2
3
4
5
6
try {
    const data = await someAsynchronousFunction();
} catch(err) {
    // If promise is rejected
    console.error(err);
}

¡Esto se parece mucho a JavaScript síncrono "regular"! Puede obtener más información sobre async/await en nuestro artículo, Node.js Async Await en ES7.

Las bibliotecas de JavaScript más populares y los nuevos proyectos usan Promises con las palabras clave async/await.

Sin embargo, si está actualizando un repositorio existente o encuentra un código base heredado, probablemente le interese mover las API basadas en devolución de llamada a API basadas en Promise para mejorar su experiencia de desarrollo. Tu equipo también te lo agradecerá.

¡Veamos un par de métodos para convertir las devoluciones de llamada en promesas!

Convertir una devolución de llamada en una promesa {#convertir una devolución de llamada en una promesa}

Node.js Promisify

La mayoría de las funciones asincrónicas que aceptan una devolución de llamada en Node.js, como el módulo fs (sistema de archivos), tienen un estilo de implementación estándar: la devolución de llamada se pasa como el último parámetro.

Por ejemplo, así es como puedes leer un archivo usando fs.readFile() sin especificar la codificación del texto:

1
2
3
4
5
6
7
8
9
fs.readFile('./sample.txt', (err, data) => {
    if (err) {
        console.error(err);
          return;
    }

    // Data is a buffer
    console.log(data);
});

Nota: si especifica utf-8 como codificación, obtendrá una salida de cadena. Si no especifica la codificación, obtendrá una salida Buffer.

Además, la devolución de llamada, que se pasa a la función, debería aceptar un Error ya que es el primer parámetro. Después de eso, puede haber cualquier número de salidas.

Si la función que necesita convertir en una Promesa sigue esas reglas, puede usar util.promise , un módulo nativo de Node.js que oculta las devoluciones de llamadas a Promises.

Para hacer eso, primero importa el módulo util:

1
const util = require('util');

Luego usas el método promisify para convertirlo en una promesa:

1
2
const fs = require('fs');
const readFile = util.promisify(fs.readFile);

Ahora use la función recién creada como una promesa regular:

1
2
3
4
5
6
7
readFile('./sample.txt', 'utf-8')
    .then(data => {
        console.log(data);
    })
    .catch(err => {
        console.log(err);
    });

Alternativamente, puede usar las palabras clave async/await como se indica en el siguiente ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const fs = require('fs');
const util = require('util');

const readFile = util.promisify(fs.readFile);

(async () => {
    try {
        const content = await readFile('./sample.txt', 'utf-8');
        console.log(content);
    } catch (err) {
        console.error(err);
    }
})();

Solo puede usar la palabra clave await dentro de una función que se creó con async, por lo que tenemos un contenedor de función en este ejemplo. Este contenedor de funciones también se conoce como Expresiones de funciones invocadas inmediatamente.

Si su devolución de llamada no sigue ese estándar en particular, no se preocupe. La función util.promisify() puede permitirle personalizar cómo ocurre la conversión.

Nota: Las promesas se hicieron populares poco después de su presentación. Node.js ya ha convertido la mayoría, si no todas, de sus funciones principales de una devolución de llamada a una API basada en Promise.

Si necesita trabajar con archivos usando Promises, use la biblioteca que viene con Node.js.

Hasta ahora, ha aprendido a convertir las devoluciones de llamada de estilo estándar de Node.js en promesas. Este módulo solo está disponible en Node.js desde la versión 8 en adelante. Si está trabajando en el navegador o en una versión anterior de Node, probablemente sería mejor para usted crear su propia versión de la función basada en promesas.

Creando tu promesa

Hablemos de cómo encubrir las devoluciones de llamada a las promesas si la función util.promisify() no está disponible.

La idea es crear un nuevo objeto Promise que se ajuste a la función de devolución de llamada. Si la función de devolución de llamada devuelve un error, rechazamos la Promesa con el error. Si la función de devolución de llamada devuelve una salida sin errores, resolvemos la Promesa con la salida.

Comencemos convirtiendo una devolución de llamada en una promesa para una función que acepta un número fijo de parámetros:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const fs = require('fs');

const readFile = (fileName, encoding) => {
    return new Promise((resolve, reject) => {
        fs.readFile(fileName, encoding, (err, data) => {
            if (err) {
                return reject(err);
            }

            resolve(data);
        });
    });
}

readFile('./sample.txt')
    .then(data => {
        console.log(data);
    })
    .catch(err => {
        console.log(err);
    });

Nuestra nueva función readFile() acepta los dos argumentos que hemos estado usando para leer archivos con fs.readFile(). Luego creamos un nuevo objeto Promise que envuelve la función, que acepta la devolución de llamada, en este caso, fs.readFile().

En lugar de devolver un error, “rechazamos” la Promesa. En lugar de registrar los datos inmediatamente, resolvemos la Promesa. Luego usamos nuestra función readFile() basada en Promise como antes.

Probemos con otra función que acepte un número dinámico de parámetros:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const getMaxCustom = (callback, ...args) => {
    let max = -Infinity;

    for (let i of args) {
        if (i > max) {
            max = i;
        }
    }

    callback(max);
}

getMaxCustom((max) => { console.log('Max is ' + max) }, 10, 2, 23, 1, 111, 20);

El parámetro de devolución de llamada también es el primer parámetro, lo que lo hace un poco inusual con funciones que aceptan devoluciones de llamada.

La conversión a una promesa se realiza de la misma manera. Creamos un nuevo objeto Promise que envuelve nuestra función que usa una devolución de llamada. Luego, “rechazamos” si encontramos un error y “resolvemos” cuando tenemos el resultado.

Nuestra versión prometida se ve así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const getMaxPromise = (...args) => {
    return new Promise((resolve) => {
        getMaxCustom((max) => {
            resolve(max);
        }, ...args);
    });
}

getMaxCustom(10, 2, 23, 1, 111, 20)
    .then(max => console.log(max));

Al crear nuestra promesa, no importa si la función utiliza devoluciones de llamada de forma no estándar o con muchos argumentos. Tenemos el control total de cómo se hace y los principios son los mismos.

Conclusión

Si bien las devoluciones de llamada han sido la forma predeterminada de aprovechar el código asíncrono en JavaScript, Promises es un método más moderno que los desarrolladores creen que es más fácil de usar. Si alguna vez encontramos un código base que usa devoluciones de llamada, ahora podemos hacer que esa función sea una Promesa.

En este artículo, vio por primera vez cómo usar el método utils.promisfy() en Node.js para convertir funciones que aceptan devoluciones de llamada en Promises. Luego vio cómo crear su propio objeto Promise que envuelve una función que acepta una devolución de llamada sin el uso de bibliotecas externas.

¡Con esto, una gran cantidad de código JavaScript heredado se puede mezclar fácilmente con bases de código y prácticas más modernas! Como siempre, el código fuente está disponible en GitHub. omises).