Have you heard about Envirotechnical? Let's fight climate change one byte at a time :) Learn More →

logo
Published on

Let's settle all the Promises, a better DX and higher readability

Authors
Table of Contents

Async calls

Considering the fact that we used to deal with Async Javascript code with quite a few tricky escamotages it is with a light smile that I look back at callback pyramids.

Promises have pretty much always been there, Node implemented them early on but didn't quite integrate them well until ES2017 with the advent of async and await, which made everyone's world a better place.

I remember a few years ago when Promises came out and they were not yet fully implemented based on the ECMAScript standards in the native Node installation, we had to rely upon amazing libraries like Bluebird, which, apparently, are still more performant and fully compliant with the ECMAScript specs.

Promise API

The Promise API is pretty straightforward:

You have a Promise constructor which can hold 3 states. Pending, Fulfilled, Rejected (cries inside).

Each state tells you, in order:

  • whether your Promise is ongoing and you are still waiting for the response, without knowing if it worked or failed (Pending)
  • if your Promise worked out, everyone's happy and you received an answer from it (Fulfilled)
  • if it failed, for any programming logic that was used to deal with it (Rejected)

To create a promise:

const fs = require('fs')

const nonBlockingReadFile = (fileName) => {
  return new Promise((resolve, reject) => {
    fs.readFile(fileName, (err, data) => {
      if (err) {
        reject(err)
        return
      }
      resolve(data)
    })
  })
}

nonBlockingReadFile('./config.json')
  .then((data) => console.log(data))
  .catch((err) => console.error(err))

This way we turned Node's fs package readFile method into a promise function call. Pretty neat because we're not blocking the Event Loop as we would have with readFileSync, but we can still write code kind of in a "sync" way.

We used then instead of the callback pyramid and it's definitely more readable, but we can do a lot better!

// Top level await

const nonBlockingReadFile = (fileName) => {
  // ... as implemented above
}

const readData = await nonBlockingReadFile('./config.json')

We could do that already with the utils/promisify package or by using fs-promises package but this was good enough for an example, wasn't it.

Actually Node >= 11 implemented promises directly into the fs package, so now you can just do:

// Top level await

const fs = require('fs').promises

const readData = await fs.readFile('./config.json')

If you wanted to iterate through a series of promises you might think that this is a good implementation idea:

// Top level await
const promiseOperations = [p1, p2, p3]

for (let i = 0; i < n; i++) {
  await promiseOperations[i]
}

It would work, and maybe that's exactly what you want. As of now, considering the example just above, we are effectively awaiting on the promise to be either rejected or fulfilled before dealing with the other promises in the array of promiseOperations.

This would actually work as expected. Await on promise 1, do what you want with the results, continue.

But what if you didn't need to deal with the results of the single promises while they were being awaited, which is usually the case?

Enter cool methods (read it as if you were reading Shakespeare, thank you very much)

Promise.all()

Promise.all() takes an iterable as argument and that iterable must be full of pending promises, just like your new year's resolutions.

// Top level await
const newYearResolutions = [promise1, promise2, promise3]

Promise.all(newYearResolutions).then((actuallyDone) => {
  console.log(actuallyDone)
})

If you managed to actually keep up with what you promised, the console.log will show you the things that you put out to do and that you've actually done. That is, if you have done everything you promised yourself.

This is actually good when you want to make sure that all your promises are fulfilled and you want to catch any promise, even a single one, that might get rejected. In fact, Promise.all() just stops and throws an error if any of the promises gets rejected.

Promise.allSettled()

But, since we both know that promises can be broken, maybe we should settle a little bit with accepting that some of them might be fulfilled and some others might not. A rejected promise is still a promise that gets answered, although not as wanted.

// Top level await
const newYearResolutions = [promise1, promise2, promise3]

Promise.allSettled(newYearResolutions).then((actuallyDone) => {
  console.log(actuallyDone)
})

In this scenario we will have answers from all of our promises, no matter the state. This is probably more accurate as a model for your new year's resolutions, isn't it?

Promise.any()

But since you are a hero of promises and resolutions, this is the one that goes well with new year's resolutions:

// Top level await
const newYearResolutions = [promiseThatYouWillActuallyTakeCareOf, promise2, promise3]

Promise.any(newYearResolutions).then((actuallyDone) => {
  console.log(actuallyDone) //
})

With Promise.any() we are actually taking care of the fact that any promise, whichever it is in the list, can make you a 100% compliant with what you promised.

In a list of iterables full of Promise objects you just need to have a single one that gets resolved and voilà, returned values and success for your resolutions. Well, just one resolution, but you can always tell everyone that you actually did keep at least one promise.

The goodbye

I hope you found this article useful and to your liking and if you have any requests, drop a message on one of my social media accounts or open an issue/start a discussion on github, on this repository!

As always you can find me on Twitter, listen to my Podcast on Spotify and add me on LinkedIn to talk professionally (yeah, right)