DEV Community

saroj sasmal
saroj sasmal

Posted on

Mastering Javascript Promises

A promise is an object that represents the eventual completion or failure of an asynchronous operation. It simply means that we can write asynchronous code using a promise, just like we do with a callback function but with some ease and most importantly without getting into the trap of callback hell 😉.

What is a Promise?

A promise is a construct to execute code asynchronously, which may be in one of the following states at a given point in time.

  • Pending:- Initial state, neither fulfilled nor rejected.
  • Fulfilled:- Successful execution, returns value via then handler.
  • Rejected:- Failure, can be handled using a catch handler.
return new Promise((resolve, reject) => {
  setTimeout(() => resolve("done"), 1000);
})
Enter fullscreen mode Exit fullscreen mode

The above code uses a setTimeout that resolves the promise with a value "done" in this case in one second.

Consider the following code that fetches GitHub user information using promise.


 function fetchUser(username) {
  fetch('https://api.github.com/users/' + username)
  .then(response => response.json())
  .then( data => {
    const str = JSON.stringify(data, undefined, 2);
    document.getElementById('content').innerHTML = str;
  })
  .catch( error => console.error(error));
}

fetchUser('saroj990');

Enter fullscreen mode Exit fullscreen mode

Initially, promises were not baked into native Javascript(es6 got promise built natively into javascript) rather were available via third-party libraries like Q, BlueBird. So all the libraries that had been developed back in those days probably had used a dedicated separate promise library for achieving asynchronicity.

How to Construct a Promise?

We just need to create a new instance of Promise, which receives resolve and reject as arguments and when we want to return a value, we use resolve and reject is used to reject the promise with an error.


function doAsync() {
  return new Promise((resolve, reject) => {
    const number =  Math.ceil(Math.random() * 10);
    if (number % 2 === 0) {
      setTimeout(() => resolve("even"), 2000);
    } else {
      setTimeout(() => reject("odd"), 2000);
    }
  });
}

Enter fullscreen mode Exit fullscreen mode

We are sort of calculating a random number between 1 to 10. If the number turns out to be an even number, we resolve the promise. If the value is odd, we reject the promise.

Here is how we can execute a promise.

doAsync()
  .then((value) => {
    // success handler
  })
  .catch(err => {
    //log error
  });

Enter fullscreen mode Exit fullscreen mode

When we resolve a promise then the value is received by the then handler and in case of rejection, the error is caught by the catch handler.

Why do we need a Promise?

If you already know this👌👌. But I will keep it short here so that we don't get deviated from our topic.

Promises were introduced to mitigate the problems that emerged by callback hell.

Callback Hell

Callbacks are nothing but functions that can be passed into another function as an argument, and when there are more callbacks nested one inside another, the code becomes really hard to understand.

function getUser(id, profile, callback) {
  User.find(id, function (err, user) {
    if(err) {
      callback(err);
    } else {
      user.profile = profile;
      user.save(function(err, user) {
        if(err) { 
          callback(err)
        } else {
          Subscription.findSubscription(id, function(err, subscription) {
            if(err) {
              callback(err) ;
            } else {
              user.subscription = subscription;
              callback(subscription);
            }
          });
        }
      });
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

The above code looks bad and not expressive at all, the situation becomes really worse when another level of nesting comes into the picture.

Let's re-factor the same code with a promise.

function getUser(id, profile) {
  const currentUser = {};
  return new Promise((resolve, reject) => {
    User
      .find(id)
      .then((user) => {
        currentUser = user;
        currentUser.profile = profile })
      .then(() => Subscription.find(id))
      .then(subscription => {
        currentUser.subscription = subscription;
        return resolve(currentUser)
      })
      .catch(err => reject(err))
  })

}
Enter fullscreen mode Exit fullscreen mode

Now the code looks really neat👌👌. Isn't it ?. So using a promise has an added advantage as it makes your code more readable and easy to understand.

Chaining a Promise

Promise chaining is a pattern where the output of one promise becomes an input for another.

Here is an example where we are kind of trying to book an appointment.


Appointment
.findSlot(time)
.then(slot => BookAnAppointment(slot.id))
.then(appointment => FinishPayment(appointment.id))
.then(payment => getInvoice(payment.id))
.then(invoice => console.log(invoice))
.catch(err => console.log(err));
Enter fullscreen mode Exit fullscreen mode

Parallel executions

There are situations where promises need to be executed independently and have no relation with other promises.

There is a Promise.all construct in Javascript promise that executes promises in parallel for achieving this.

// marks a user in-active
function markInActive(id) {
  return User
  .findById(id)
  .then(user => {
    user.active = false;
    //returns a promise
    return user.save();
  });
}

// collect the promises into an array
const promises = []
for (let i=0; i < ids.length; i++) {
  promises.push(markInActive(ids[i]));
}

//execute them altogether
Promise.all(promises)
.then(result => console.log(result))
.catch(error => console.log(error));

Enter fullscreen mode Exit fullscreen mode

You might be wondering what is the difference between chaining a promise vs parallel execution. Well, let's evaluate it with an example.


function promiseOne() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve('promiseOne'), 1000);
  })
}

function promiseTwo() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve('promiseTwo'), 1000);
  })
}

function promiseThree() {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve('promiseThree'), 1000);
  })
}
Enter fullscreen mode Exit fullscreen mode

When the promises are executed in a chain, the second promise starts its execution only when the first promise finishes.

promiseOne()
.then((res1) => { 
  console.log(res1);
  return promiseTwo() 
})
.then((res2) => {
  console.log(res2);
  return promiseThree();
}).then(res3 => {
  console.log(res3);
})
.catch(err => console.log(err));

/*
output
promiseOne
promiseTwo
promiseThree
each promise takes 1sec to execute
effective time: 3sec
*/
Enter fullscreen mode Exit fullscreen mode

Now let's try the same code with Promise.all, parallel execution allows all the promises to run in parallel at the same time.


Promise.all([ promiseOne(), promiseTwo(), promiseThree()])
  .then(result => console.log(result))
  .catch(err => console.log(err));


/*
output: 
[ 'promiseOne', 'promiseTwo', 'promiseThree' ]
all the promises get executed at the same time
so effective time: 1sec
*/
Enter fullscreen mode Exit fullscreen mode

Converting a Callback to a Promise

If you've followed along up to this point, you should know how to convert a callback to a promise. First off, we need to know why do we need to convert a callback to a promise.

There are times where certain library functions don't have their promise variant methods(i doubt almost all libraries ship their promise interface method these days), but you want to use it as a promise.

function saveUser(payload) {
  return new Promise((resolve, reject) => {
    User.save(payload, function(err, user) {
      if(err) return reject(err);
      return resolve(user);
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

The User model save method is a callback method, we just wrapped it inside a new Promise construct with resolve and reject. if an error happens, we reject the promise with error, else we just resolve it with user information.

Error Handling(catch/finally)

Although creating a promise is fun, it will be useless if we don't handle errors that may occur while executing a promise. To achieve this, we have the catch handler at our disposal, which receives the error object as an argument to the handler function.

Here is a sample code that explicitly throws an error and it is handled by the catch block.

new Promise((resolve, reject) => {
  reject("some error happened!");
}).catch(err => console.log(err));

Enter fullscreen mode Exit fullscreen mode

We can also throw an explicit error from the promise and it is exactly the same as above.

new Promise((resolve, reject) => {
  throw new Error("some error occurred!!")
}).catch(err => console.log(err));
Enter fullscreen mode Exit fullscreen mode

A catch handler can handle both synchronous or asynchronous occurred inside a program.

What we just saw in the above example where we deliberately raised an error. Now let's take look at another example where the error is asynchronous.

const prom1 = () => new Promise((resolve, reject) =>  {
  setTimeout(() => {
    //rejects after 2sec
    return reject("rejected prom1 promise");
  }, 2000)
});

new Promise((resolve, reject) => resolve("done"))
  .then(res => prom1())
  .catch(err => console.log(err))
Enter fullscreen mode Exit fullscreen mode

Here the first method prom1 rejects the promise asynchronously(just mimicked with a setTimeout😉).

A then and catch block can be nested one after another like following.


new Promise((resolve, reject) => {
  resolve("done")
}).then(res => {
  console.log("response is : ", res);
  throw new Error("error after the first promise resolved");  // synchronous error
}).catch(err => {
  console.log("error caught in catch handler", err);
  return "You can rest now"; 
//simply pass the value to next level
}).then(res => console.log(res))
.catch(err => console.log(err)); 
// prints "you can rest now"


Enter fullscreen mode Exit fullscreen mode

Usually, people just use one catch block appended to the end of the promise, and whatever error occurs just get caught by the catch handler.

Finally

Another important part of a promise is the finally block, which gets executed no matter a promise is successful or rejected.


new Promise((resolve, reject) => resolve("done"))
.then(res => console.log(res))
.catch(err => console.log("I can catch fish too. :)"))
.finally(() => console.log("I am inevitable, I will always get a chance to execute"))

Enter fullscreen mode Exit fullscreen mode

Let me explain it in a better way with an example so that we can really get the reason behind using a finally block.

isLoading = true;
fetchUser(id)
.then(user => subscribeToNewsLetter(user.id))
.then(response => {
  console.log("subscribed to news letter", response);
  // set loader to false once the user info is retrieved
  isLoading = false;
})
.catch(err => { 
  console.log(err);
  // in case of error
  isLoading = false;
});

Enter fullscreen mode Exit fullscreen mode

We are sort of using a isLoading variable to track when an async operation starts and when it is finished so that we can display a loader and hide it when we get the response.

Needless to say, we are setting the isLoading to false in two different places.

  • inside the success handler then
  • inside the error handler. This is because if any error happens we don't want the loader to continue forever. Do you? 😂😂

This implementation works but not efficient and is repetitive. We can handle it better with a finally block.

isLoading = true;
fetchUser(id)
.then(user => subscribeToNewsLetter(user.id))
.then(response => console.log("subscribed to news letter", response))
.catch(err => console.log(err))
.finally(() => isLoading = false);

Enter fullscreen mode Exit fullscreen mode

Finally block gets executed no matter what happens to a promise, so this can be used as a place where we can do some clean-ups and stuff like closing DB, socket connections, etc.

If you have made up this far, congrats!!😁👍. If you feel like that this article has helped you understand Javascript Promises, don't hesitate to show your love by liking this post.

If you feel like something could be improved in the article, please do add a comment. I would really appreciate it.

Top comments (0)