Las promesas son una solución para implementar código asíncrono que aparece con el objetivo de reemplazar a los callbacks. Al ser una abstracción más avanzada, las promesas permiten operaciones como esperar a que diversas operaciones asíncronas terminen de ejecutarse de manera concurrente, mejoran la legibilidad del código y facilitan el manejo de errores. En definitiva, no son simplemente una moda sino que también aportan ventajas competitivas frente a los callbacks.

Entendiendo las promesas

Una promesa, como concepto, es un objeto que nos va a devolver un resultado cuando una operación haya finalizado. Este resultado puede ser o bien el valor resultante de la operación o bien un error que se ha producido durante la ejecución. En el estándar de promesas A+, la promesa se crea a partir de un callback en el que ejecutaremos aquella operación necesaria para producir dicho resultado y, en función de esta ejecución, llamaremos a las funciones pasadas como argumento resolve y reject. Cuando llamemos a resolve, se considera que la promesa ha resuelto correctamente y que la operación ha sido un éxito. Si llamamos a reject es que ha ocurrido un error y deberíamos propagar ese error como argumento de la función reject.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function readFilePromise(fileName)
{
    var promise = new Promise(function(resolve, reject) {
        fs.readFile(fileName, function(err, buffer) {
            if (err) {
                reject(err);
                return;
            }
            resolve(buffer);
        });
    });
    
    return promise;
}

var promise = readFilePromise("file.txt");
promise.then(
    function(buffer) {
        console.log("FILE CONTENTS", buffer.toString());
    },
    function(err) {
        console.log("FILE READ ERROR", err);
    }
);

De callbacks a promesas

Por poco Javascript que hayas hecho, habrás utilizado algún callback para recuperar datos desde el cliente de una web, o si también lo habéis utilizado del lado del servidor (Node.js), otros casos de uso son operaciones de entrada salida como la lectura/escritura de ficheros o acceso a base de datos.

Los callbacks tienen ciertos problemas, ya que en los casos en los que necesitamos los resultados de un callback para poder empezar a ejecutar otra operación, se suele caer en la pirámide de la muerte, que complica la legibilidad del código. El siguiente ejemplo muestra claramente un ejemplo de pirámide de la muerte.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function doSomeAsyncStuff()
{
    fs.readFile(fileName, function(err, buffer) {
        if (err) {
            logger.log("unexpected error reading file " + fileName);
            return;
        }

        var url = buffer.toString();
        http.get(url, function(err, contents) {
            if (err) {
                logger.log("unexpected error retrieving " + url, err);
                return;
            }

            db.insert('chunks', {data: data}, function(err) {
                if (err) {
                    logger.log("error inserting chunk data!", JSON.stringify({data: data}), err);
                }
            });
        });
    });
}

Las promesas resuelven este problema siguiendo una cadena de callbacks que se ejecuta al mismo nivel, mejorando la legibilidad del código.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function doSomeAsyncStuff()
{
    fs.readFile(fileName)
    .then(function(buffer) {
        var url = buffer.toString();
        http.get(url)
        .then(function(contents) {
            var dbRecord = {data: contents};

            db.insert('dumps', dbRecord)
                .catch(function(err) {
                    logger.log("error inserting contents", dbRecord, err);
                });
        }, function(err) {
            logger.log("unexpected error requesting " + url, err);
        });
        
    }, function(err) {
        logger.log("unexpected error reading file " + fileName);
    });
}

A parte de esto, en los casos totalmente opuestos en los que nos importa es esperar a que una serie de acciones concurrentes finalice, las promesas ofrecen mayor facilidad de uso.

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
function fetchMultipleUrls(urls, fn)
{
    var counter = 0;
    var errors = [];
    var results = [];

    for(var i = 0; i < urls.length; i++) {
        var url = urls[i];
        http.get(url, function(err, contents) {
            if (err) {
                errors.push({
                    error: err,
                    url: url
                });
            } else {
                results.push({
                    result: contents,
                    url: url
                });
            }

            counter++;

            if (counter == urls.length) {
                fn(errors, results);
            }
        });
    }
}

Este ejemplo se descarga una serie de URLs en paralelo. La solución utilizada necesita mucho código para propagar los resultados y errores de cada url y además nuestra solución no es estándar, por lo que el programador que debiera utilizar este método tendría que conocer como le vamos a notificar qué urls han funcionado y cuales no. Veamos el mismo ejemplo con promesas.

1
2
3
4
5
6
7
8
9
10
11
12
function fetchMultipleUrls(urls)
{
    var promises = [];

    for(var i = 0; i < urls.length; i++) {
        var url = urls[i];
        var promise = http.get(url);
        promises.push(promise);
    }

    return Promise.all(promises);
}

Con promesas, utilizamos un mecanismo estándar, que es devolver una promesa como unión de todas las promesas. Con ello, seremos notificados de si todas las peticiones han funcionado o bien si alguna de ellos ha fallado.

En general, las promesas son muy útiles cuando tenemos diferentes operaciones asíncronas en paralelo, y abstraen mucho mejor el código asíncrono que los callbacks.

Promesas en Node.js

Node.js incluye a partir de la versión 0.12 la clase nativa Promise que cumple el estándar A+, con la cual podemos utilizar promesas sin necesidad de utilizar una librería. A pesar de ello, la librería promise incorpora utilidades estándar como Promise.denodeify, para transformar una función que devuelve un callback típica de Node.js en una función que nos devuelve una promesa, facilitando la transición entre ambas interfaces.

Promesas en navegadores

Diversos navegadores implementan de manera nativa las promesas A+. Firefox, Chrome e IE+12 soportan esta funcionalidad, por lo que se puede considerar que las promesas están muy extendidas en los navegadores de escritorio. No podemos decir lo mismo para los navegadores para móvil, en los cuales ninguno soporta de manera nativa las promesas.

A pesar de ello, existen polyfills muy ligeros a utilizar en navegadores que no soporten promesas de manera nativa, por lo que la falta de soporte en el entorno mobile no debería desanimar completamente si quieres empezar a utilizar promesas.

El futuro de las promesas: async/await

Algunos lenguajes como C# implementan un modelo asíncrono dentro del propio lenguaje, con las palabras clave async/await. Google y Microsoft están trabajando para importar este modelo asíncrono a Javascript en sus navegadores basándose en las promesas A+ . Esto eliminaría completamente los callbacks, haciendo muy sencillo esperar a que una promesa resuelva su resultado. Además, async/await entran dentro del estándar EcmaScript 7 por lo que en el futuro la funcionalidad se incorporará también a otros navegadores.

Referencias