Promises - Escape from the Planet of the Callbacks

George Hotelling - Progressive Leasing

Adam
Spotify
/r/NewMusicFriday

New Music Friday Bot

  • Load New Music Friday Playlist
  • Post Playlist
  • Make Playlist Sticky
  • Post Tracks as Comments
  • Announce Success

Callbacks

spotifyApi.clientCredentialsGrant(function(err, credentials) {
  if (err) return err;

  spotifyApi.setAccessToken(credentials.body['access_token']);
  return spotifyApi.getPlaylist('spotify', 'NewMusicFriday', function postPlaylist(err, spotifyRes) {
    if (err) return err;

    return reddit('/api/submit').post({
      'api_type': 'json',
      'kind': 'link',
      'resubmit': true, 
      'sendreplies': false,
      'sr': config.reddit.subreddit,
      'title': _.get(spotifyRes, 'body.name') + ' - ' + moment().format('MMMM Do, YYYY'),
      'url': _.get(spotifyRes, 'body.external_urls.spotify')
    }, function(err, redditPostRes) {
      if (err) return err;

      return reddit('/api/set_subreddit_sticky').post({
        id: redditPostRes.json.data.name,
        num: 1,
        state: true
      }, function(err) {
        if (err) return err;

        var trackPosted = [].fill(false, 0, spotifyRes.body.tracks.items.length - 1);

        for (var i=0; i < spotifyRes.body.tracks.items.length; i++) {
          var item = spotifyRes.body.tracks.items[i];
          var artists = item.track.artists.map((a) => a.name).join(', ');
          var title = artists + ' - ' + item.track.name;

          reddit('/api/comment').post({
            'api_type': 'json',
            'text': '[' + title + '](' + _.get(item, 'track.external_urls.spotify') + ')',
            'thing_id': spotifyRes.reddit.json.data.name
          }, function(err) {
            if (err) return err;
            console.log('Posted ' + title);
            trackPosted[i] = true;
            if (trackPosted.every((trackDone) => trackDone)) {
              console.log('DONE!');
            }
          });
        }
      });
    });
  });
});

OK, Better Callbacks

spotifyApi.clientCredentialsGrant(function (err, credentials) {
    if (err) return err;

    spotifyApi.setAccessToken(credentials.body['access_token']);
    return spotifyApi.getPlaylist('spotify', 'NewMusicFriday', postPlaylist);
});

var spotifyRes;

function postPlaylist(err, spotifyResArg) {
  if (err) return err;

  spotifyRes = spotifyResArg;

  return reddit('/api/submit').post({
    'api_type': 'json',
    'kind': 'link',
    'resubmit': true,
    'sendreplies': false,
    'sr': config.reddit.subreddit,
    'title': _.get(spotifyRes, 'body.name') + ' - ' + moment().format('MMMM Do, YYYY'),
    'url': _.get(spotifyRes, 'body.external_urls.spotify')
  }, stickyPlaylist);
}

function stickyPlaylist(err, redditPostRes) {
  if (err) return err;

  return reddit('/api/set_subreddit_sticky').post({
    id: redditPostRes.json.data.name,
    num: 1,
    state: true
  }, postTracks);
}

var trackPosted;
function postTracks(err) {
  if (err) return err;

  trackPosted = [].fill(false, 0, spotifyRes.body.tracks.items.length - 1);

  for (var i = 0; i < spotifyRes.body.tracks.items.length; i++) {
    var item = spotifyRes.body.tracks.items[i];
    var artists = item.track.artists.map((a) => a.name).join(', ');
    var title = artists + ' - ' + item.track.name;

    reddit('/api/comment').post({
      'api_type': 'json',
      'text': '[' + title + '](' + _.get(item, 'track.external_urls.spotify') + ')',
      'thing_id': spotifyRes.reddit.json.data.name
    }, onTrackPosted(title, i));
  }
}

function onTrackPosted(title, i) {
  return function (err) {
    if (err) return err;
    console.log('Posted ' + title);
    trackPosted[i] = true;
    if (trackPosted.every((trackDone) => trackDone)) {
      console.log('DONE!');
    }
  }
}

Promises!

Promises

What is a Promise?

It is a Promise for a value. Eventually.

For when you want to pass around a value you don't have yet.

Promises

States

  • Pending
  • Resolved → .then()
  • Rejected → .catch()

If you have a Promise and you want to know its state you're Doing It Wrong.

How to create Promises

var promise = new Promise(function(resolve, reject) {
  try {
    …
    resolve(value);
  } catch(e) {
    reject(e);
  }
}));

Promise Superpower #1: Promise.resolve()

var resolvedPromise = Promise.resolve(foo);

Promise Superpower #2: Promise.reject()

var rejectededPromise = Promise.reject(bar);

Browser Support Dec. 2015

Can I Use graph

New Music Friday Bot

Load New Music Friday Playlist

spotifyApi.clientCredentialsGrant()
  .then(function(credentials) {
    spotifyApi.setAccessToken(credentials.body['access_token']);
    spotifyApi.getPlaylist('spotify', 'NewMusicFriday');
  })

New Music Friday Bot

Post Playlist

spotifyApi.clientCredentialsGrant()
  .then(function(credentials) {
    spotifyApi.setAccessToken(credentials.body['access_token']);
    spotifyApi.getPlaylist('spotify', 'NewMusicFriday')
      .then(function (playlist) {
        reddit('/api/submit').post({
          'api_type': 'json',
          'kind': 'link',
          'resubmit': true,
          'sendreplies': false,
          'sr': 'NewMusicFriday',
          'title': _.get(playlist, 'body.name') + ' - ' + moment().format('MMMM Do, YYYY'),
          'url': _.get(playlist, 'body.external_urls.spotify')
        });
     });
  });

Promise Anti-Pattern #1

Calling .then() inside .then()

New Music Friday Bot

Post Playlist

var playlistPromise = new Promise(function(resolve, reject) {
  spotifyApi.clientCredentialsGrant()
    .then(function(credentials) {
      spotifyApi.setAccessToken(credentials.body['access_token']);
      spotifyApi.getPlaylist('spotify', 'NewMusicFriday')
        .then(resolve)
    })
});

playlistPromise.then(function(playlist) {
  reddit('/api/submit').post({
      …
      'sr': 'NewMusicFriday',
      'title': _.get(playlist, 'body.name') + ' - ' + moment().format('MMMM Do, YYYY'),
      'url': _.get(playlist, 'body.external_urls.spotify')
  })
});

Promise Anti-Pattern #2

You (almost) never need to create a new, pending Promise!

Promise.resolve() and Promise.reject() are OK

.then() Rule #1

If you return a Promise from a .then() the outer Promise becomes the inner Promise

New Music Friday Bot

Load New Music Friday Playlist

spotifyApi.clientCredentialsGrant()
  .then(function(credentials) {
    spotifyApi.setAccessToken(credentials.body['access_token']);
    return spotifyApi.getPlaylist('spotify', 'NewMusicFriday');
  })
  .then(function(playlist) {
    reddit('/api/submit').post({
        'api_type': 'json',
        'kind': 'link',
        'resubmit': true,
        'sendreplies': false,
        'sr': 'NewMusicFriday',
        'title': _.get(playlist, 'body.name') + ' - ' + moment().format('MMMM Do, YYYY'),
        'url': _.get(playlist, 'body.external_urls.spotify')
    })
  })

New Music Friday Bot

Load New Music Friday Playlist

var credentialPromise = spotifyApi.clientCredentialsGrant();
var playlistPromise = credentialPromise.then(function(credentials) {
  spotifyApi.setAccessToken(credentials.body['access_token']);
  return spotifyApi.getPlaylist('spotify', 'NewMusicFriday');
});

var redditPostPromise = playlistPromise.then(function(playlist) {
  reddit('/api/submit').post({
      'api_type': 'json',
      'kind': 'link',
      'resubmit': true,
      'sendreplies': false,
      'sr': 'NewMusicFriday',
      'title': _.get(playlist, 'body.name') + ' - ' + moment().format('MMMM Do, YYYY'),
      'url': _.get(playlist, 'body.external_urls.spotify')
  })
});

.then() Rule #2

If you return nothing from a .then(), the Promise value stays the same.

New Music Friday Bot

Refactor!

spotifyApi.clientCredentialsGrant()
  .then(saveAccessToken)
  .then(getPlaylist)
  .then(postPlaylist)
  
function saveAccessToken(credentials) {
  spotifyApi.setAccessToken(credentials.body['access_token']);
}

function getPlaylist() {
  return spotifyApi.getPlaylist('spotify', 'NewMusicFriday');
}

function postPlaylist(playlist) {
  return reddit('/api/submit').post({…});
}

New Music Friday Bot

Sticky Playlist

spotifyApi.clientCredentialsGrant()
  .then(saveAccessToken)
  .then(getPlaylist)
  .then(postPlaylist)
  .then(stickyPlaylist)
…
function stickyPlaylist(redditPostRes) {
  return reddit('/api/set_subreddit_sticky').post({
    id: redditPostRes.json.data.name,
    num: 1,
    state: true
  });
}

New Music Friday Bot

Post Tracks

spotifyApi.clientCredentialsGrant()
  .then(saveAccessToken)
  .then(getPlaylist)
  .then(postPlaylist)
  .then(stickyPlaylist)
  .then(postTracks)
…
function postTracks(???) {

}

Promise Superpower #3: Promise.all()

Refactor!

spotifyApi.clientCredentialsGrant()
  .then(saveAccessToken)
  .then(getPlaylist)
  .then(postPlaylist)
  .then(stickyPlaylist)
…
function postPlaylist(playlist) {
  return Promise.all([playlist, reddit('/api/submit').post({…})]);
}

function stickyPlaylist(playlist, redditPost) {
  return Promise.all([playlist, redditPost, reddit('/api/set_subreddit_sticky').post({…})]);
}

New Music Friday Bot

Post Tracks

spotifyApi.clientCredentialsGrant()
  .then(saveAccessToken)
  .then(getPlaylist)
  .then(postPlaylist)
  .then(stickyPlaylist)
  .then(postTracks)
…
function postTracks(playlist, redditPost) {
    return Promise.all(playlist.body.tracks.items.map(function(item) {
        var artists = item.track.artists.map((a) => a.name).join(', ');
        var title = artists + ' - ' + item.track.name;

        return reddit('/api/comment').post({
                'api_type': 'json',
                'text': '[' + title + '](' + _.get(item, 'track.external_urls.spotify') + ')',
                'thing_id': redditPost.json.data.name
            })
            .then(function() {
                console.log('Posted ' + title);
            });
    }));
}

Error Handling

spotifyApi.clientCredentialsGrant(function (err, credentials) {
    if (err) return err;
    …
});
function postPlaylist(err, spotifyResArg) {
    if (err) return err;
…
}

function stickyPlaylist(err, redditPostRes) {
    if (err) return err;
…
}

function postTracks(err) {
    if (err) return err;
…
}

function onTrackPosted(title, i) {
    return function (err) {
        if (err) return err;
…
        }
    }
}

Error Handling

spotifyApi.clientCredentialsGrant()
  .then(saveAccessToken)
  .then(getPlaylist)
  .then(postPlaylist)
  .then(stickyPlaylist)
  .then(postTracks)
  .then(console.log.bind(console, 'DONE!'))
  .catch(console.error.bind(console))

Anti-Bieber Code

function postTracks(playlist, redditPost) {
  return Promise.all(playlist.body.tracks.items.map(function(item) {
    var artists = item.track.artists.map((a) => a.name).join(', ');
    if (/bieber/i.test(artists)) {
      throw 'Not a Belieber!';
    }
    var title = artists + ' - ' + item.track.name;
    return reddit('/api/comment').post({
      'api_type': 'json',
      'text': '[' + title + '](' + _.get(item, 'track.external_urls.spotify') + ')',
      'thing_id': redditPost.json.data.name
    })
    .then(function() {
      console.log('Posted ' + title);
    });
  }));
}

Promise Anti-Pattern #3

Not having a .catch() statement.

The Future!

🔮

The Future!

async

await

The Future!

try {
  let credentials = await spotifyApi.clientCredentialsGrant();
  spotifyApi.setAccessToken(credentials.body['access_token']);
  let playlist = await getPlaylist();
  let redditPost = await postPlaylist(playlist);
  await Promise.all([
    stickyPlaylist(playlist, redditPost),
    postTracks(playlist, redditPost)
  ]);
  console.log('DONE!');
} catch (err) {
  console.error(err);
}

async function postPlaylist(playlist) {
  return reddit('/api/submit').post({…});
}

Closing

.then() Rules:

  1. If you return a Promise from a .then() the outer Promise becomes the inner Promise
  2. If you return nothing from a .then(), the Promise value stays the same.
  3. If you return a non-Promise value from a .then(), the Promise value becomes that value

Promise Superpowers

  1. Promise.resovle()
  2. Promise.reject()
  3. Promise.all()
  4. Promise.race() - first value in array to resolve

Promise Anti-Patterns

  1. Calling .then() inside .then()
  2. You (almost) never need to create a new, pending Promise!
  3. Not having a .catch() statement.

Resources

Questions?

Slides up on https://github.com/georgeh/

Ask in the #javascript room in Slack