Is it possible to compose promises in JavaScript?

Viewed 416

I'm trying to turn my imperative procedural code into a more functional code. My question is how can I compose promises? For example I have this code from my Discord bot:

LyricsCommand.searchSong(songName)
      .then(function(url) {
        LyricsCommand.getSongPageURL(url)
          .then(function(url) {
            LyricsCommand.getLyrics(url)
              .then(function(lyrics) {
              // .......... do something ..........
              })
              .catch(function(err) {
                message.reply(err);
                return;
              });
          })
          .catch(function(err) {
            message.reply(err);
            return;
          });
      })
      .catch(function(err) {
        message.reply(err);
        return;
      });
3 Answers

Whenever you return from a .then() clause, the returned value is wrapped with a promise, so you can chain promises together. You also need a only single .catch() clause to handle all errors in the chain before it.

In your case you can use LyricsCommand.getSongPageURL, and LyricsCommand.getLyrics as the callback for .then(), since they need only the previous value (url), and will return a url for the next step. The message.reply function can used be in the same way for the .catch() block.

LyricsCommand.searchSong(songName)
  .then(LyricsCommand.getSongPageURL)
  .then(LyricsCommand.getLyrics)
  .then(function(lyrics) {
    // .......... do something ..........
  })
  .catch(message.reply);

If you want to compose promises in a functional way, you'll need to create a compose function that can handle promises (taken from this article).

const composePromise = (...functions) =>
  initialValue =>
    functions.reduceRight(
      (sum, fn) => Promise.resolve(sum).then(fn),
      initialValue
    );

Example (using SpaceX API):

const composePromise = (...functions) =>
  initialValue => functions.reduceRight(
    (sum, fn) => Promise.resolve(sum).then(fn),
    initialValue
  )
  
const fn = composePromise(
  console.log,
  data => data.rocket.rocket_id,
  res => res.json(),
  url => fetch(url, { method: 'get' })
)

const url = 'https://api.spacexdata.com/v2/launches/latest';

fn(url).catch(console.log)

This is how you'll use composePromise with your code:

const fn = composePromise(
  lyrics => // do something with lyrics
  LyricsCommand.getLyrics,
  LyricsCommand.getSongPageURL,
  LyricsCommand.searchSong
)

and you'll call it like this:

fn(songName).catch(message.reply)

Couple of options. Since each of those functions returns a Promise, you can flatten the promise chain, rather than nesting them.

LyricsCommand.searchSong(songName)
  .then(url => LyricsCommand.getSongPageURL(url))
  .then(url => LyricsCommand.getLyrics(url))
  .then(function(lyrics) {
    // .......... do something ..........
  })
  .catch(function(err) {
    message.reply(err);
  });

The single .catch will work for the entire chain.

Alternatively, you could use the async/await syntax:

async function getSong(songName) {
  try {
    const url = await LyricsCommand.searchSong(songName);
    const url2 = await LyricsCommand.getSongPageURL(url);
    const lyrics = await LyricsCommand.getLyrics(url2);
    // .......... do something ..........
  } catch {
    message.reply(err);
  }
}

I think this is what you're looking for:

async function doStuffWithLyrics(songName) {
    try {
        const song = await LyricsCommand.searchSong(songName);
        const songPage = await LyricsCommand.getSongPage(song);
        const lyrics = await LyricsCommand.getLyrics(songPage);
    
        // Do stuff with lyrics
    } catch (err) {
       message.reply(err);
    }
}

If you really wanted to, you could do something like:

async function doStuffWithLyrics(songName) {
    try {
        const lyrics = await LyricsCommand.getLyrics(await LyricsCommand.getSongPage(await LyricsCommand.searchSong(songName)));

        // Do stuff with lyrics
    } catch (err) {
       message.reply(err);
    }
}

but it doesn't save you much and I'd find it harder to read.

Related