DEV Community

Juan Cruz Martinez
Juan Cruz Martinez

Posted on • Originally published at livecodestream.dev on

Understanding Promises in JavaScript

We can’t go into the JavaScript world without ever having to deal with Promises, and for some users, somehow, even unknowingly they have been using Promises throughout their codes thanks to the power of abstraction. However, when dealing with any JavaScript application, it’s important we understand what a promise is, how it works, and the power behind it.

But before we jump in, let’s take a look to the following pseudo-code:

const promiseToReader = new Promise((resolve, reject) => {

    setTimeout(function() {
        if (userLikedTheArticle) {
            resolve('This article is awesome!')
        } else {
            reject('I should have never been here! ;p')
        }
    }, enoughToReadArticle)

})

I hope you could appreciate a little bit my JavaScript humor :p, but that piece of code is quite important, as it’s our first encounter with Promises.

What is a Promise?

The Promise object represents the eventual completion (or failure) of an asynchronous operation, and it’s resulting value. More precisely is a proxy for a value not necessarily known when the promise is created. It allows for asynchronous action’s eventual success value or failure reason.To put it into easier words, though not technically correct, it’s like a function, which would run in parallel, and return its value once it’s done, allowing the block who made the call to continue even though the result of the function is not yet available.

A Promise can be in any of the following states:

  • pending: initial state, neither fulfilled nor rejected.
  • fulfilled: meaning that the operation completed successfully.
  • rejected: meaning that the operation failed.

An interesting behavior o Promises is that they can be chain. The following diagram shows the life-cycle of a Promise and how the chaining concept works:

How do we create Promises?

Let’s start at the construction definition:

/**
 * Creates a new Promise.
 * @param executor A callback used to initialize the promise. This callback is passed two arguments:
 * a resolve callback used to resolve the promise with a value or the result of another promise,
 * and a reject callback used to reject the promise with a provided reason or error.
 */
new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;

So what does any of that means? let’s see it in a blank example

const welcomeToPromises = new Promise((resolve, reject) => {
    // This is the body if the asynchronous operation
    // When this operation finishes we can call resolve(...) to set the Promise as fulfilled
    // In case of failure, we can call reject(...) to set the Promise as rejected

    // Let's do now something async, like waiting for a second
    // Though in reallity you would probably do a remote operation, like XHR or an HTML5 API.
    setTimeout(() => {
        resolve('Sucess!') // Super! all went well!
    }, 1000)
}

So far we just created the promise, and the code in the body started its execution. After 1 second, the Promise will change it’s state as fulfilled, however our resolve("Success!") won’t trigger anything in particular, as our code is concerned. If we want to capture the result of the promise, then we need to do something else, we need to tell the object to fire a function after the state changes.

Consuming Promises, then and catch

We use the object methods then and catch to work with promises. The method then will register a callback, which will be executed automatically after the promise gets either resolved or rejected, while the method catch will register a call back, which will be fired when the Promise fails.

Let’s again see the declarations for both methods:

/**
 * Attaches callbacks for the resolution and/or rejection of the Promise.
 * @param onfulfilled The callback to execute when the Promise is resolved.
 * @param onrejected The callback to execute when the Promise is rejected.
 * @returns A Promise for the completion of which ever callback is executed.
 */
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;

/**
 * Attaches a callback for only the rejection of the Promise.
 * @param onrejected The callback to execute when the Promise is rejected.
 * @returns A Promise for the completion of the callback.
 */
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;

Let’s see them in action with our promiseToReader example:

promiseToReader.then(result => {
    console.log('result', result)
}, error => {
    console.error('error in then', error)
})

promiseToReader.catch(error => {
    console.error('error', error)
})

In our case, we registered a then callback, and a catch callback. In the case of the then block, we can register 2 callbacks, one for success and one for failure, and in the catch, we can only register a failure callback.

The callback is simply a function with an argument, the result of the operation, with a value in the case of the fulfillment and a reason in the case of failure, though this is just a convention, technically we can return whatever we want.

So what would happen to our example?

Well… If the userLikedTheArticle the then callback will be fired, and we will see in the console the result. However, if !userLikedTheArticle, then the failure callback will be called, in this case twice, once for the then method, and another for the catch method.

Now let’s assume we encapsulate all this code into a test(userLikedTheArticle) function, and we call this function twice as follows:

console.log('Initiating test...')
console.log('Good reader started reading...')
test(true)
console.log('Probably what is a bot started reading...')
test(false)
console.log('Both users are now reading, and soon I should get my results')

What do you think would be the output?

Initiating test...  
Good reader started reading...  
Probably what is a bot started reading...   
Both users are now reading, and soon I should get my results    
result  This article is awesome!    
error in then   I should have never been here! ;p   
error   I should have never been here! ;p

Awesome! But read well and pay attention to the order in the execution. Though test(true) was called before console.log("Probably what is a bot started reading..."), the latter was printed first on the screen, this is because the promise, was still resolving, and thus not fulfilled when it reached that line of code. It’s fascinating, and you can see it live, and play with it in the demo:

Promise Chaining

Because .then() or .catch() always return a new promise, it’s possible to chain promises with precise control over how and when errors are handled. Why would this be helpful? Imagine a situation where the user enters a URL on a form, and we need to retrieve, and process information from that URL, how would you go about doing that? It could be something like this:

fetch(url)
  .then(validate)
  .then(process)
  .catch(handleErrors)

It looks beautiful and lets us build an easy pipeline of asynchronous tasks.

Avoid code duplication with Finally

In addition to then and catch, promises expose a 3rd method to register a callback, finally, with the following declaration syntax:

finally?<U>(onFinally?: () => U | Promise<U>): Promise<U>;

These callbacks, similar to the previous ones will return a promise, so it can be chained, and it will be executed when the promise settles, either with fulfillment or failure. These callbacks are great for cleaning up, or switching loaders off. They are very easy to use:

promiseToReader.finally(function() {
    // settled (fulfilled or rejected)
    console.log("Finally settled!")
});

How do I cancel a Promise?

The short answer is: you don’t! Promises by design cannot be canceled, however, some very clever people thought of some methods which can simulate a cancellation. There’re even libraries which can help with this task, but since in essence the promise is never canceled I won’t cover them here.

Extras

In addition to the promise object, Promise offers a series of static methods that can help you work easier with promises, let’s take a look into them:

Promise.all(iterable):Wait for all promises to be resolved, or for any to be rejected.

If the returned promise resolves, it is resolved with an aggregating array of the values from the resolved promises, in the same order as defined in the iterable of multiple promises.

If it rejects, it is rejected with the reason from the first promise in the iterable that was rejected.

Promise.allSettled(iterable):Wait until all promises have settled (each may resolve or reject).

Returns a promise that resolves after all of the given promises have either resolved or rejected, with an array of objects that each describe the outcome of each promise.

Promise.race(iterable):Wait until any of the promises is resolved or rejected.

If the returned promise resolves, it is resolved with the value of the first promise in the iterable that resolved.

If it rejects, it is rejected with the reason from the first promise that was rejected.

Promise.reject(reason):Returns a new Promise object that is rejected with the given reason.

Promise.resolve(value):Returns a new Promise object that is resolved with the given value. If the value is a thenable (i.e. has a then method), the returned promise will “follow” that thenable, adopting its eventual state; otherwise the returned promise will be fulfilled with the value.

Generally, if you don’t know if a value is a promise or not, Promise.resolve(value) it instead and work with the return value as a promise.

Async / Await

When chaining promises, code can get out of hand, and it could be very hard to read. So some people thought of another way to work with promises, using async functions and the await operator.

Let’s see them in definitions:

  • Await Operator: Is used to wait for a Promise. It can only be used inside an async function.
  • Async function: is a function declared with the async keyword. Async functions are instances of the AsyncFunction constructor, and the await keyword is permitted within them. The async and await keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains.

But an example is always better, so let’s jump into it

function resolveAfter2Seconds() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('resolved');
    }, 2000);
  });
}

async function asyncCall() {
  console.log('calling');
  const result = await resolveAfter2Seconds();
  console.log(result);
  // expected output: 'resolved'
}

asyncCall();

What do you think the result will be?

> "calling"
> "resolved"

In difference to the previous example, in here the call execution “stopped” waiting for the Promise to be settled before continuing. This syntax avoids using callbacks and can bring a lot of clarity to the code.


Conclusion

Promises are widely used in JavaScript, and today most browsers would offer a native implementation of the Promise API, only older browsers such as some version of IE would require a polyfilll or similar to work.Promises are very fun to work with, though can be really hard to grasp initially. Practice, practice and practice, there are many use cases for this feature, and with no doubt any JS developer should know them in detail. It’s a classic interview question, so be prepared.

Thanks so much for reading!

Top comments (0)