Probably you’ve already heard about the new kid on the block: promises.

But, what the heck is a promise?

Before we get in touch with this guy, let’s contextualize the situation and understand where it’s applicable.


Callbacks, callbacks and more callbacks

Let’s suppose we have a time consuming operation, like a database access, parse some file, etc. You, as a good programmer, won’t let user waiting for this operation to finish. So, you decided to provide a callback to this time consuming method.

A callback is nothing more than a function that will be called once this time consuming operation has finished.

Just to exemplify this situation, let’s consider the function below:

// Callback success if time < 4000.
// Otherwise, callback error
const MAX_SUCCESS_TIME = 4000

const timeConsumingOperation = (id, success, error) => {
  // Generate a random value between 1000 and 5000
  // (used in interval)
  const time = Math.round((Math.random() * 4000) + 1000)

  setTimeout(() => {
    if (time < MAX_SUCCESS_TIME) success({id: id, time: time})
    else                         error  ({id: id, time: time})
  }, time)
}

The function above receives 3 arguments: an ID (used to identify the process), an two callbacks, one for success and other for error.

It generates a random value, between 1000 and 5000 (1 and 5 seconds). This value is then send to setTimeout() method.

After the timeout is complete, the appropriate callback is called: if the time generate was less than 4000 (4 seconds), success callback is invoked. Otherwise, error callback is invoked.

So, calling this time consuming function, as below…

timeConsumingOperation(0,
  (result) => console.log(`Operation finished successfully in ${result.time}ms`),
  (error)  => console.log(`Operation finished unsuccessfully: ${error.time}ms > ${MAX_SUCCESS_TIME}ms`)
)

… we get as result after 3 executions:

Operation finished successfully in 3659ms
Operation finished successfully in 2274ms
Operation finished unsuccessfully: 4289ms > 4000ms

No problem until here.

But now, let’s consider a more complex situation:

Let’s suppose that you’re registering a new user in you platform, and, so, you need to perform 3 time consuming operations:

  • Operation 1: Create many records on database, like user profile, account, preferences, etc;
  • Operation 2: Validate user credit card;
  • Operation 3: Send welcome email;

So, let’s use our new branch function with callbacks to perform that.

timeConsumingOperation(1,
  (result) => {
    console.log(`Operation ${result.id} finished in ${result.time}ms`)
    timeConsumingOperation(2,
      (result) => {
        console.log(`Operation ${result.id} finished in ${result.time}ms`)
        timeConsumingOperation(3,
          (result) => {
            console.log(`Operation ${result.id} finished in ${result.time}ms`)
            console.log(`All 3 operations finished successfully in ${new Date().getTime() - initialTime}ms`)
          }
        )
      }
    )
  }
)

WOW, what a mess!

We have 3 main problems here:

  • This is a clearly convoluted code, since, for each new call we add, another step in the ladder is added;
  • We don’t have error handling here. So, adding the error callback for each time consuming call will turn this code in a spaghetti (and this is not cool, even if this code is going to be used in an Italian restaurant website)
  • The worst one: this algorithm is now synchronous, since the next call is just performed once the previous one is finished

There must be a better and cleaner way to do this…

Yes, it does, and this is called promises…


Promises

Promise is a technique to perform asynchronous operations in a composable way.

A promise, instead returning the result value of the operation, represents a operation that hasn’t completed yet. It’s like saying:

I haven’t finished my task yet but, as soon I do, I promise that I’ll return a success or an error response.

A promise can be in 3 different states:

  • Pending: not yet finished
  • Fulfilled: execution finished with success
  • Rejected: execution finished with error

Once a promise has its status changed to fulfilled or rejected, this is the final state.


Creating a promise

In order to create a promise, we simply instantiate a new promise object, passing 2 callbacks as arguments: the first one is the resolve callback and the seconds one is the reject callback. Check it out:

function myBrandNewFunction() {
  return Promise.new((resolve, reject) => {
           if ([success]) resolve()
           else           reject()
         })
}

Now we have to handle the both situations: the success (resolve) and failure (reject).

Success operations are handled by the method then(), while error operations are handled by the method catch().

So, using our function myBrandNewFunction() as example:

myBrandNewFunction()
  .then(()  => console.log('Promise resolved'))
  .catch(() => console.log('Promise rejected'))

In the example above, myBrandNewFunction() returns a promise. As soon this promise is created, it’s in pending status, meaning that the execution isn’t finished yet.

Since the execution is done, the function decides which callback call: the resolve one or the reject.

So, we now just need to treat each case: the success one (with then()) and the failure one (with catch()).

We can also chain then() calls, where the input of the current then() is a promise provided by the previous one.

For example:

myBrandNewFunction()
  .then(() => {
    console.log('Log message 1')
    return Promise.resolve()
  })
  .then(() => {
    console.log('Log message 2')
    return Promise.resolve()
  })
  .then(() => {
    console.log('Log message 3')
    return Promise.reject()
  })
  .catch(() => console.log('Promise rejected'))

Back to our problem

So, now that we know what a promise is and what’s its purpose, let’s adjust our ugly algorithm to use promises and see the result.

Let’s make our time consuming operation return a promise instead calling a callback:

// Callback success if time < 4000
// Otherwise, callback error
const MAX_SUCCESS_TIME = 4000

const timeConsumingOperation = (id) => {
  // Generate a random value between 1000 and 5000
  // (used in interval)
  const time = Math.round((Math.random() * 4000) + 1000)

  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (time < MAX_SUCCESS_TIME) resolve({id: id, time: time})
      else                         reject ({id: id, time: time})
    }, time)
  })
}

Notice that our argument list has reduced from 3 to 1 element, because we don’t need provide callbacks for this method anymore.

Now, let’s handle the promise resolution:

timeConsumingOperation(1)
  .then((result) => console.log(`Operation finished successfully in ${result.time}ms`))
  .catch((error) => console.log(`Operation ${error.id} failed: ${error.time}ms > ${MAX_SUCCESS_TIME}ms`))

A simple and clean solution :)

But, what about our last problem:

Run 5 time consuming operations and log out a message when all of them finished successfully?

Well, no problem at all my friend!

Promises class provide a method called all, which basically waits for all promises to finished and, then:

  • Resolve it, if all promises resolve, returning an array with all resolved values;
  • Reject it, if at least one of them rejected, returning the reject value;

So, our spaghetti algorithm from the callback example can be simplified by this one, using promises:

Promise.all([
  timeConsumingOperation(1),
  timeConsumingOperation(2),
  timeConsumingOperation(3)
])
.then ((results) => {
  results.forEach((result) => console.log(`Operation ${result.id} finished successfully in ${result.time}ms`))
})
.catch((error) => console.log(`Operation ${error.id} failed: ${error.time}ms > ${MAX_SUCCESS_TIME}ms`))

Now, our code is:

  • Callbacks free: no more need to deal with callbacks. Promise handle that for us;
  • Not convoluted: it’s deadly easy to add more operations and handle their results;
  • Clean: much more easy to understand the logic and maintain the code;
  • Asynchronous: all operations are dispatched in parallel, and Promise.all takes care of controlling everything and resolving/rejecting each operation;

Promise class has a lot of other helpful methods. You can check all of them here.


Conclusion

Promises can be a little confusing in a first moment, but once you get familiar with the technique, it surely can be very helpful, allowing you to write (or even rewrite) your algorithms in a much cleaner and maintainable way.

All the examples presented here are available on GitHub