Promesas en Node.js

Una promesa es una función asíncrona que da su palabra de que se devolverá un valor en un momento posterior. Presentado en ES6/ES2015, Promises ofrece una alternativa moderna a las devoluciones de llamada

Introducción

JavaScript es de un solo subproceso, lo que significa que todo, incluidos los eventos, se ejecuta en el mismo subproceso. Si el subproceso no está libre, la ejecución del código se retrasa hasta que lo esté. Esto puede ser un cuello de botella para nuestra aplicación ya que realmente puede causar serios problemas de rendimiento.

Hay diferentes maneras por las cuales podemos superar esta limitación. En este artículo, exploraremos la forma moderna de manejar tareas asincrónicas en JavaScript: Promises.

Devoluciones de llamada e infierno de devolución de llamada

Si es un desarrollador de JavaScript, es probable que haya oído hablar de devoluciones de llamada, si no las usa:

1
2
3
4
5
function hello() {
    console.log('Hello World!');
}

setTimeout(hello, 5000);

Este código ejecuta una función, setTimeout(), que espera el tiempo definido (en milisegundos), que se le pasa como segundo argumento, 5000. Pasado el tiempo, solo entonces ejecuta la función hola, que se le pasa como primer parámetro.

La función es un ejemplo de una función de orden superior y la función que se le pasa se llama devolución de llamada, una función que debe ejecutarse después de que otra función haya terminado de ejecutarse.

Digamos que enviamos una solicitud a una API para devolver las fotos que más gustan de nuestra cuenta. Es probable que tengamos que esperar la respuesta, ya que la API o el servicio pueden estar haciendo algunos cálculos antes de devolver la respuesta.

Esto puede llevar mucho tiempo y no queremos congelar el hilo mientras esperamos la respuesta. En su lugar, crearemos una devolución de llamada que será notificada cuando llegue la respuesta.

Hasta ese momento, el resto del código se está ejecutando, como presentar publicaciones y notificaciones.

If you've ever worked with callbacks, there's a chance you've experienced infierno de devolución de llamada:

1
2
3
4
5
6
7
8
9
doSomething(function(x) {
    console.log(x);
    doSomethingMore(x, function(y) {
        console.log(y);
        doRestOfTheThings(y, function(z) {
            console.log(z);
        });
    });
});

Imagine un caso en el que solicitamos al servidor que obtenga múltiples recursos: una persona, sus amigos y las publicaciones de sus amigos, los comentarios de las publicaciones de cada amigo, las respuestas, etc.

La gestión de estas dependencias anidadas puede salirse rápidamente de control.

Podemos evitar los infiernos de devolución de llamada y manejar llamadas asincrónicas usando Promises.

Creación de una promesa

Promises, como su nombre lo indica, es la función "dando su palabra" de que se devolverá un valor en un momento posterior. Es un proxy para un valor que podría no devolverse, si la función de la que esperamos una respuesta no se entrega.

En lugar de devolver valores concretos, estas funciones asincrónicas devuelven un objeto Promesa, que en algún momento se cumplirá o no.

La mayoría de las veces, al codificar, estaremos consumiendo Promises en lugar de crearlas. Son las bibliotecas/marcos los que crean Promises para que los clientes las consuman.

Aún así, es bueno entender qué hay detrás de la creación de una Promesa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let promise = new Promise(function(resolve, reject) {
    // Some imaginary 2000 ms timeout simulating a db call
    setTimeout(()=> {
        if (/* if promise can be fulfilled */) {
            resolve({msg: 'It works', data: 'some data'});
        } else {
            // If promise can not be fulfilled due to some errors like network failure
            reject(new Error({msg: 'It does not work'}));
        }
    }, 2000);
});

El constructor de la promesa recibe un argumento: una devolución de llamada. La devolución de llamada puede ser una función regular o una función de flecha. La devolución de llamada toma dos parámetros: resolver y rechazar. Ambos son referencias a funciones. La devolución de llamada también se denomina ejecutor.

El ejecutor se ejecuta inmediatamente cuando se crea una promesa. La promesa se resuelve llamando a resolve() si se cumple la promesa, y se rechaza llamando a reject() si no se puede cumplir.

Tanto resolve() como reject() toman un argumento: booleano, cadena, número, matriz o un objeto.

Consumir una promesa {#consumir una promesa}

A través de una API, supongamos que solicitamos algunos datos del servidor y no está claro cuándo se devolverán, si es que se devolverán. Este es un ejemplo perfecto de cuando usaríamos una Promesa para ayudarnos.

Asumiendo que el método del servidor que maneja nuestra llamada devuelve una Promesa, podemos consumirla:

1
2
3
4
5
promise.then((result) => {
    console.log("Success", result);
}).catch((error) => {
    console.log("Error", error);
})

Como podemos ver, hemos encadenado dos métodos: then() y catch(). Estos son algunos de los diversos métodos proporcionados por el objeto Promise.

then() se ejecuta cuando las cosas van bien, es decir, la promesa se cumple con el método resolve(). Y si la promesa fue rechazada, se llamará al método catch() con el error enviado a reject.

Encadenando promesas

Si tenemos una secuencia de tareas asincrónicas una tras otra que deben realizarse, cuanto más anidamiento hay, más confuso se vuelve el código.

Esto nos lleva al infierno de devolución de llamada, que se puede evitar fácilmente encadenando varios métodos then() en un único resultado Promised:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
promise.then(function(result) {
    // Register user
    return {account: 'blahblahblah'};
}).then(function(result) {
    // Auto login
    return {session: 'sjhgssgsg16775vhg765'};
}).then(function(result) {
    // Present WhatsNew and some options
    return {whatsnew: {}, options: {}};
}).then(function(result) {
    // Remember the user Choices
    return {msg: 'All done'};
});

Como podemos ver, el resultado se pasa a través de la cadena de controladores then():

  • El objeto promesa inicial se resuelve
  • Luego se llama al manejador then() para registrar al usuario
  • El valor que devuelve se pasa al siguiente controlador then() para iniciar sesión automáticamente en el usuario
  • ...y así

Además, then(handler) puede crear y devolver una promesa.

Nota: Aunque técnicamente podemos hacer algo como el ejemplo de procedimiento, puede quitarle el punto de encadenamiento. Aunque esta técnica puede ser buena para cuando necesite llamar opcionalmente a métodos asincrónicos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
let promise = new Promise(function(resolve, reject) {
    setTimeout(() => resolve({msg: 'To do some more job'}), 1000);
});

promise.then(function(result) {
    return {data: 'some data'};
});

promise.then(function(result) {
    return {data: 'some other data'};
});

promise.then(function(result) {
    return {data: 'some more data'};
});

Lo que estamos haciendo aquí es simplemente agregar varios controladores a una promesa, todos los cuales procesan el “resultado” de forma independiente. No se pasan el resultado entre sí en la secuencia.

De esta forma, todos los controladores obtienen el mismo resultado, el resultado de esa promesa: {msg: 'To do some more job'}.

Conclusión

Promises, como su nombre lo indica, es la función "dando su palabra" de que se devolverá un valor en un momento posterior. Es un proxy para un valor que podría no devolverse, si la función de la que esperamos una respuesta no se entrega.

En lugar de devolver valores concretos, estas funciones asincrónicas devuelven un objeto ‘Promesa’, que en algún momento se cumplirá o no.

Si ha trabajado con devoluciones de llamada, debe apreciar la semántica limpia y clara de Promises.

Como desarrollador de Node/JavaScript, nos ocuparemos de las promesas con más frecuencia. Después de todo, es un mundo asincrónico, lleno de sorpresas.