FE / js / Promise

Oh My Promises

I’ve been shamingly using Promises for years without really taking the time to explore beyond then and catch.

Big big mistake. There’s so much more. then and catch are just the tip of the iceberg.

Promise, the main idea

Let’s begin considering what’s a Promise.
Loosely speaking, think of a Promise as an object which may deliver a value.

As the name Promise suggests, the concept behind is: I don’t have a value now but I promise to deliver once I have it.

As such, a Promise can be in one of these states:

  • pending: initial state, on waiting.
  • fulfilled: operation resolved, successfully completed (on success).
  • rejected: operation rejected, failed (on failed).

When the Promise is either fulfilled or rejected can be called settled. Note that settled is not a state per se, but just a convenient term you can use when you want to say a Promise is completed.

At this point you may feel the harshness of the definition, so let’s use an analogy to clear things up.

Imagine a friend who wants to borrow some money from you. If you agree, your friend promises to repay the money in 5 days.

Assuming you lend the money, you’ll be in the pending state (on waiting) for 5 days.

After 5 days, two things could happen. One, your friend returns the money, in that case, the return money operation is successfully resolved (fulfilled). Two, something happens and your friend is not able to return the money, the return money operation failed (rejected).

In pseudo-code it could look like this:

var returnMoneyPromiseA = 
    new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve('event on success, friend return money')
        }, 5_DAYS_IN_MILLISEC);
    });

// When returnMoneyPromiseA is created is in pending state
// Promise {<pending>}
// this is because your friend will return the money in 5 days (setTimout)
// for the next 5 days returnMoneyPromiseA is in state `pending`

// After 5 days returnMoneyPromiseA will be
// Promise {<fulfilled>: 'event on success, friend return money'}


var returnMoneyPromiseB = 
    new Promise((resolve, reject) => {
        setTimeout(() => {
          reject('event on failure, friend not able to return money')
        }, 5_DAYS_IN_MILLISEC);
    });

// When returnMoneyPromiseB is created is in pending state
// Promise {<pending>}
// this is because your friend should return the money in 5 days 
// for the next 5 days returnMoneyPromiseB is in state `pending`

// Your friend couldn't give you the money back. 
// After 5 days returnMoneyPromiseB will be
// Promise {<rejected>: 'event on failure, friend not able to return money'}      

then

Let’s talk now about the most used method within promises: the then method.

By itself then is just a method that accepts two arguments:

  1. the function to call on fulfilment (equivalent of on success) and
  2. the function to call on reject (equivalent of on failure).
// Thenable interface
// Any object that has a then method.

then(onFulfilment, onReject) : PromiseLike
then(onSuccess, onFailure) : PromiseLike

The key point to remember is that then always return another thenable object, that is a Promise.

This is the most important™ and underrated detail about promises so I’ll repeat, then always return another Promise. Even when your onFulfilment returns a number, that will be cast to a fulfilled Promise.

This is powerful because you can keep passing the value to the next then and so you can use Promise to chain multiple functions till you get the desired result.

Note: The example uses the arrow function implicit return. If you need to refresh your memory about it read Arrow function expressions: Function body

// Pseudo code implements thenable interface
// See how a cast to promise could work  
class Thenable {
    constructor(value) {
        this.value = value;
    }

    then(onFulfilment, onReject) {
        const nextVal = onFulfilment(this.value);

        return new Thenable(nextVal); 
    }
}

// Example of chaining functions
(new Thenable(6))
  .then((val) => val + 5)          // arrow function implicit return, returns 11
  .then((val) => console.log(val)) // print in console 11


// Promise propagation and function chaining example
var p0 = new Promise((resolve) => resolve(5))
Promise{<fulfilled>: 5}

var p1 = p0.then(a => a + 1)
Promise{<fulfilled>: 6}

var p2 = p1.then(b => b + 2)
Promise{<fulfilled>: 8}

// all together
(new Promise((resolve) => resolve(5)))
  .then(a => a + 1)
  .then(b => b + 2)
  .then(console.log)  // print in console 8

In the first example, you can see how the value coming from onFulfilment gets cast in a Thenable, the same happens in the second example where the returned value gets cast to a fulfilled Promise.

Catch

Now that we saw how the then method works we can start with the catch method.

catch is very similar to then, so similar that it’s nothing more than a then method with only the onReject function.

// Equivalent syntax
catch(onReject)
then(undefined, onReject)

As then, catch will always return another fulfilled Promise.

An interesting case with catch is when you chain multiple then and one of them throw an error as the error stops the chain propagation and triggers the first catch.

This is convenient as it allows avoiding unnecessary operations.

But before seeing a catch example, let’s talk for a moment about Promise.resolve as it will help with the explanation.

Promise.resolve returns a fulfilled Promise which you can use to start the chain of then. This is useful when you have a normal value but you still want to use the chains of then.

Technically speaking, Promise.resolve resolves a value to a fulfilled Promise.

// Equivalent Syntax
// no setTimeout, no delay 
var A = Promise.resolve(5);

var B = new Promise((resolve, reject) => {
            resolve(5)
        });
// Promise {<fulfilled>: 5}

With Promise.resolve in our toolbelt, let’s see a more interesting example.

// Let's assume we're receiving some user input.
// For example it could be a postcode and we want 
// to perform some validation

const toString = (value) => '' + value;
const trim = (value) => value.trim();
const isNotEmpty = (value) => {
    if (value.length === 0) {
        throw Error('Postcode can not be empty');
    }

    return value;
};

const is4DigitLong = (value) => {
    if (value.length !== 4) {
        throw Error('Postcode must be 4 digits long');
    }

    return value;
}


Promise.resolve(' a ') // "resolves" text to a Promise 
  .then(toString)      // force cast to string 
  .then(trim)          // trim
  .then(isNotEmpty)    // pass check
  .then(is4DigitLong)  // throw error 'Postcode must be 4 digits long'
  .catch(console.log)  // print in console 'Postcode must be 4 digits long'


Promise.resolve('  ') // "resolves" text to a Promise 
  .then(toString)      // force cast to string 
  .then(trim)          // trim
  .then(isNotEmpty)    // throw error 'Postcode can not be empty'
  .then(is4DigitLong)  // - skipped because the previous error
  .catch(console.log)  // print in console 'Postcode can not be empty'

In the example, we created an input validator merely using Promise.resolve, catch and the chain of then.

This should give you a sense of how easy and convenient it is to split and chain functions using then and catch.

What makes it so easy and convenient? Well, because all the functions within the then are pure functions with no side effects, no global states, just plain input/output.

Aggregators

As you start dealing with group of promises there may be times when you may want to do something if the group is in a specific state.

In this type of situation aggregators, a set of Promise static methods, are here to help you.

To get an idea about how they work let’s examine Promise.all.

Promise.all allows you to get a group of promises and wait until all of them are successfully completed. Promise.all by itself returns a new Promise and if any of the promises in the group fails then Promise.all fails too.

So, let’s extend the input example above to a form example

// Let's assume you have a form with multiple inputs, each one with some validation
// in case of a validation fail you want to disable the submit button

function validatePostcode(value) {
    return Promise.resolve(value)  
        .then(toString)       
        .then(trim)          
        .then(isNotEmpty)    
        .then(is4DigitLong)  
        .catch(() => (showErrorMessage(), Promise.reject("postcode-error")));

        // Note how we use `Promise.reject` to propagate the 
        // rejection to `Promise.all` as by itself catch would
        // return a fulfilled Promise, which would not trigger 
        // the catch at the `Promise.all` level.
}


// You can imagine several functions in the same spirit of validatePostcode
Promise.all([
    validateName(inputName.value),
    validateLastName(inputLastName.value),
    validateStreet(inputStreet.value),
    validatePostcode(inputStreet.value),
])
  .then(enableSubmitButton)    // if all the promises are fulfilled  
  .catch(disableSubmitButton); // if any promise is rejected 

As you can see, you can use Promise.all to create abstractions on top of group of promises which you can use to trigger actions.
The beauty of this is that you’re still using promises.

Summary

We’re close to the end so it’s time to recap.
We saw the general concept of a Promise, the states it can be in, the then method, catch, Promise.resolve() and Promise.all().

With these concepts in mind, you should be able to explore the docs. Probably the most interesting parts are going to be Promise.prototype.finally() and the rest of the aggregator functions like Promise.allSettled, Promise.race and Promise.any.

If you liked what you saw here it may interest you RxJS as well, which is basically Promises with steroids. If the idea tingled your curiosity you can check RxJS in less than 5 mins.

Comments