Tyler Benziger

Promises

Nov 10 2014


I don’t know if everybody is using them yet, but if you’re putting off like I was, here are two things about Promises that may convince you to start using them:

  1. Error handling
  2. Collections

Note: I’ve only really used bluebird so apologies if these things aren’t universal to all Promise libraries

Error handling

If you’ve done a crazy amount of nested callbacks before, you know what this one’s about. Handling errors all the way down a trail of nested callbacks is fairly straightforward, but it’s so repetitive that it can be tempting to not do it at all.

Example (yes, my examples are NodeJS-specific):

// app is an express or restify app
app.get( '/route', function( req, res, next ) {
    fs.readFile( 'path/to/file.json', { encoding: 'utf8' }, function( err, contents ) {
        if ( err ) {
            return next( err );
        }

        var data = JSON.parse( contents );

        request( 'http://service.com/api/get_info?parameter=' + data.parameter, function( err, response ) {
            if ( err ) {
                return next( err );
            }
            if ( response.statusCode !== 200 ) {
                err = new Error( response.body );
                return next( err );
            }

            res.send( response.body );
        });
    });
});

First of all, we know nested callbacks are a bad idea. Second of all, this whole if error throw error... thing is repetitive and unnecessary with the right abstraction.

Same example with promises:

app.get( '/route', function( req, res, next ) {
    fs.readFileAsync( 'path/to/file.json', { encoding: 'utf8' } )
    .then( function( contents ) {
        var data = JSON.parse( contents );

        // This example is about error handling, but this feature
        // of promises is awesome. If a then callback returns a
        // promise, the next then callback will have that promise's
        // resolved values as its arguments.
        return requestAsync( 'http://service.com/api/get_info?parameter=' + data.parameter );
    })
    .then( function( response ) {
        if ( response.statusCode !== 200 ) {
            var err = new Error( response.statusCode + ': ' + response.body );
            throw err;
        }

        res.send( response.body );
    })
    .catch( function( err ) {
        next( err );
    });
});

Much cleaner, no? Notice too, that I’m using Express error handling here. In the first example, I call next repeatedly. In the Promisified version, it’s only called once.

Collections

Without promises:

app.get( '/assets', function( req, res, next ) {
    var ids = req.query.ids.split( ',' );
    var len = ids.len;
    var data = [];
    var stop = false;

    var dataCallback = function( err, response ) {
        if ( err ) {
            stop = true;
            next( err );
        }

        if ( !stop ) {
            data.push( response );
            if ( data.length >= len ) {
                res.send({
                    request_id: req.requestId,
                    data: data
                });
            }
        }
    };

    _.each( ids, function( id ) {
        metadataService.getInfo( id, dataCallback );
    });
});

This is the coolest… Watch this:

app.get( '/assets', function( req, res, next ) {
    Promise.all(
        _.map( req.query.ids.split( ',' ), function( id ) {
            return $metadataService.getInfo( id );
        });
    )
    .then( function( data ) {
        res.send({
            request_id: req.requestId,
            data: data
        });
    })
    .catch( function( err ) {
        next( err );
    });
});

What’s going on here? The endpoint is set up to take a comma-separated list of image ids. I create an array of promises by calling $metadataService.getInfo for each id. Promise.all lets you wrap an array of promises and bind to the completion of all of them. I can’t tell you what a lifesaver this has been!