JavaScript Promises - Explain Like I'm Five

JavaScript Promises - Explain Like I'm Five

JavaScript Promise is an important topic to learn. None of the interviews are complete without promises. Let's start efficiently learning them.

ยท

10 min read

Featured on Hashnode

Hello friends ๐Ÿ‘‹, welcome to the first article of my brand new series, Demystifying JavaScript Promises - A New Way to Learn. JavaScript promises are very special. As web developers, we hardly have a way to avoid learning about it. Believe me, if not you; your interviewers indeed love promises ๐Ÿ˜‰!

On the other hand, If we take a poll on the "Hardest JavaScript concept someone is dealing with?" you would see 'promise' is making its way towards the top of the list. Don't you believe me? Here is a recent poll result ๐Ÿ™‚.

On LinkedIn

LinkedIn Poll.png

On Twitter

Twitter Poll.png

Hence it certainly makes promises 'the topic' of discussion. In this series, you will learn about JavaScript Promises from beginners to advanced levels. We will cover,

  • What is Promise, and what are its characteristics?
  • Promise Chain with examples.
  • How to deal with errors in Promises?
  • Mistakes you may make in using Promises.
  • How to prepare for your (promise) interviews?

This article will cover the basic understanding of JavaScript promise and its characteristics, mainly in a beginner-friendly way.

If you like to learn from video content as well, this article is also available as a video tutorial here: ๐Ÿ™‚

The Jack and Jill Story

The "Jack and Jill Went Up the Hill..." rhyme has two primary characters, Jack the small boy and his sister Jill. Let's twist the story. Let's introduce their grandparents.

So, Jack & Jill promise their grandparents to fetch some water from the well at the top of the hill. They started on their mission to get it. In the meantime, the grandparents are busy discussing the daily routine and want to start cooking once the kids are back with the water.

The Jack and Jill Story ๐Ÿ’ก The illustration above is my improvisation on the famous Jack and Jill rhyme. Any similarities of it with anything in this world are purely coincidental. ๐Ÿ™‚

Now there are two possibilities,

  • Jack and Jill come down with the water, and the cooking starts.
  • "Jack fell down and broke his crown. And Jill came tumbling after." - In this case, Jack and Jill return, but unfortunately, they do not get the water.

In this short story, there is a promise of getting the water using the activity of fetching it. The promise can get fulfilled(getting the water) by the kids or be rejected due to the disaster. Please note that while Jack and Jill were working on executing the promise, the grandparents were not sitting idle. They were planning the day.

The JavaScript promises also work similarly. As developers, we create them to fetch something(data from a data store, configurations, and many more). Usually, the fetching may not happen instantly. We want to fetch things asynchronously. It means we do not want the application to wait for the response, but we can continue to work on the response when it is available.

Hence our analogy table may look like this,

In Real Life(with JavaScript)In Our Story
PromiseWater fetching by Jack ๐Ÿ‘ฆ and Jill ๐Ÿ‘ง
Executor FunctionFetch the Water ๐Ÿƒโ€โ™€๏ธ ๐Ÿƒโ€โ™‚๏ธ
ActivityFetch ๐Ÿงถ
Expected data in responseWater ๐Ÿ’ง
ConsumersGrandparents ๐Ÿ‘ต ๐Ÿ‘ด
resolve/fulfilledโœ”๏ธ Successfully get the water for cooking
reject/rejectedโŒ Disaster(error) in getting the water
Task after getting the data successfullyCooking ๐Ÿš

Don't worry if some of the terms look new or confusing to you. We will revisit it at the end of this article.

Promise in JavaScript

A promise is a JavaScript object that allows you to make asynchronous(aka async) calls. It produces a value when the async operation completes successfully or produces an error if it doesn't complete.

You can create promise using the constructor method,

let promise = new Promise(function(resolve, reject) {    
    // Do something and either resolve or reject
});

We need to pass a function to the Promise Constructor. That function is called the executor function(Remember, fetching the water?). The executor function takes two arguments, resolve and reject. These two are callback functions for the executor to announce an outcome.

The resolve method indicates successful completion of the task(fetching water), and the reject method indicates an error(the disaster). You do not implement the resolve/reject method. JavaScript provides it to you. You need to call them from the executor function.

So, in the case of the Jack and Jill story, the executor function may look like this,

  • Example of the resolve:

    let promise = new Promise(function(resolve, reject) {
        // Got the water
        let value = 'water';
        resolve(value); // An assurance of getting the water successfully
    });
    
  • Example of the reject:

    let promise = new Promise(function(resolve, reject) {
        // OOPS, Jack fell down and broke his crown. 
        // And Jill came tumbling after.
        reject(new Error("Disaster")); // Throwing and error
    });
    

The Promise object and States

In the Jack and Jill story, the grandparents were not waiting for the kids to fetch the water. They were planning the day in the meantime. But Jack and Jill informed them in both the cases of getting the water successfully or meeting with the disaster. Also, the grandparents were the water consumers to cook the food.

Similarly, the promise object should be capable of informing the consumers when the execution has been started, completed (resolved), or returned with an error (rejected).

A promise object has the following internal properties,

  1. state: This property can have the following values,
    • pending: When the execution function starts. In our story, when Jack and Jill start to fetch the water.
    • fulfilled: When the promise resolves successfully. Like, Jack and Jill are back with the water.
    • rejected: When the promise rejects. Example. Jack and Jill couldn't complete the mission.
  2. result: This property can have the following values,
    • undefined: Initially, when the state value is pending.
    • value: When the promise is resolved(value).
    • error: When the promise is rejected.

A promise that is either resolved or rejected is called settled.

states_2.png

So the consumers(like the grandparents) need to rely on the promise object to know the state and value/error.

Handling Promises by the Consumers

The promise object returned by the new Promise constructor has it all. A consumer can use it to know the state(pending, fulfilled, or rejected) and its possible outcomes(value or error).

But hold on. These properties are internal. They are code-inaccessible, but they are inspectable. It means that we will be able to inspect the state and result property values using a debugger tool, but we will not be able to access them directly using the program.

So then? That's where we have three important handler methods, .then(), .catch(), and .finally(). These methods help us create a link between the executor and the consumer when a promise resolves or rejected.

executor-consumer.png

The .then() Promise Handler

We get a .then() method from every promise. The sole purpose of this method is to let the consumer know about the outcome of a promise. It accepts two functions as arguments, result and error.

promise.then(
  (result) => { 
     console.log(result);
  },
  (error) => { 
     console.log(error);
  }
);

If you are just interested in the successful outcome, you can chose to pass only one argument,

promise.then(
  (result) => { 
      console.log(result);
  }
);

Similarly, if you are interested in only the error, pass null as the value for the first argument.

promise.then(
  null,
  (error) => { 
      console.log(error)
  }
);

It is a bit odd syntax to pass a null explicitly for an error case. That's where we have an alternative called the .catch() method we will see soon.

Also, note, that you can do three very exceptional things inside the .then() method,

  • You can return another promise from it.
  • You can return a value including undefined.
  • You can throw an error.

These three points will be the basis of learning the Promise Chain in the future article. Now, let's write the code for Jack and Jill, fulfilling the promise of getting water to their grandparents.

// 1. Create a Promise to fetch the water
let promise = new Promise(function(resolve, reject) {
 // Pretend a delay of 2 sec to fetch it!
  setTimeout(function() {
      // Fetched the water. Let's resolve the promise
      resolve('Hurray! Fetched the Water.');
  }, 2000);
});

// 2. Function to Set up the handler to handle a promise result.
// It is to inform the grandparents when the result is available.
const grandParentsCooking = () => {
  // The handler function to handle the resolved promise
  promise.then(function(result) {
    // Fetched the water. Now grandparents can start the cooking
    console.log(`cooking rice with the ${result}`);
  });
}

// 3. Calling the function to activate the set up.
grandParentsCooking();

The Output,

cooking rice with the Hurray! Fetched the Water.

So, three things happen in the above code,

  1. We create the promise. In the executor function, we delay 2 seconds to pretend an async call(actually, climbing hills and fetching water takes a lot more!). Then we resolve the promise by saying, 'Hurray! Fetched the Water.'

  2. We have set up an information mechanism for the grandparents to know when the water is fetched successfully. We use the .then() handler for this purpose. Once they get the water, they start cooking. Note, here we define it, not calling it yet.

  3. Activating the handler by calling the function.

The .catch() Promise Handler

This handler method can handle errors (rejections) from promises. As we already discussed, it is a much better syntax to handle the error situation than using the .then() method. So let us now handle the "Jack fell down..." situation using JavaScript promise.

// 1. Create the promise
let promise = new Promise(function(resolve, reject) {
  setTimeout(function() {
      // Reject it as the disaster happend.
      reject(new Error('Jack fell down and broke his crown. And Jill came tumbling after.'));
  }, 2000);
});

// 2. Inform grandparents 
// but this time we are using the .catch
const grandParentsCooking = () => {
  promise.catch(function(error) {
    console.error(`OMG ${error.message}`);
  });
}

// 3. Call the function
grandParentsCooking();

The Output,

image.png

A few points to note,

  • We use the reject method in the above code to reject the promise.
  • You can pass any argument to the reject method like the resolve method. However, it is recommended to use the Error objects. We will discuss it in detail in the future article on error handling with promise.
  • We use the .catch() handler to handle the rejection. In the real world, you will have both .then() and .catch() methods to handle the resolve and reject scenarios. We will learn it in the promise chaining article of the series.

The .finally() Promise Handler

The .finally() handler method performs cleanups like stopping a loader, closing a live connection, etc. The .finally() method will be called whether a promise resolves or is rejected.

let loading = true;
loading && console.log('Loading...');

// Getting the promise
promise = getPromise();

promise.finally(() => {
    loading = false;
    console.log(`Promise Settled and loading is ${loading}`);
}).then((result) => {
    console.log({result});
});

The vital point to note, the .finally() method passes through the result or error to the next handler, which can call a .then() or .catch() again. It is convenient, and we will see many examples in the promise chain article.

In Summary

To Summarize,

  • Promise is an important building block for the asynchronous concept in JavaScript.
  • You can create a promise using the constructor function.
  • The constructor accepts an executor function as an argument and returns a promise object.
  • A promise object has two internal properties, state and result. These properties are not code-accessible.
  • The consumer of a promise can use the .then(), .catch(), and .finally() methods to handle promises.
  • The Promise is better understood using examples, like the Jack and Jill Story.

I hope now you can better relate to the analogy table.

In Real Life(with JavaScript)In Our Story
PromiseWater fetching by Jack ๐Ÿ‘ฆ and Jill ๐Ÿ‘ง
Executor FunctionFetch the Water ๐Ÿƒโ€โ™€๏ธ ๐Ÿƒโ€โ™‚๏ธ
ActivityFetch ๐Ÿงถ
Expected data in responseWater ๐Ÿ’ง
ConsumersGrandparents ๐Ÿ‘ต ๐Ÿ‘ด
resolve/fulfilledโœ”๏ธ Successfully get the water for cooking
reject/rejectedโŒ Disaster(error) in getting the water
Task after getting the data successfullyCooking ๐Ÿš

That's all for now. Please stay tuned for the second article of the series. We will learn about the Promise Chain with another story.


I hope you enjoyed this article or found it helpful. Let's connect. Please find me on Twitter(@tapasadhikary), sharing thoughts, tips, and code practices. Please give a follow. You can hit the Subscribe button at the top of the page to get an email notification on my latest posts.

You may also like,

Did you find this article valuable?

Support GreenRoots Blog - Tapas Adhikary by becoming a sponsor. Any amount is appreciated!