What the hell are promises?

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 contextualise 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