DEV Community

Watson
Watson

Posted on • Edited on

I Promise Your Deep Understanding of Promise

TL;DR

I describe background system of asynchronous function in Javascript and how to use promise a little.

Introduction

You heard that Javascript is single thread and asynchronous model so many times. But we can fetch the data from the server while computing some data or event. Someone mistakenly believe that multi threading enables it but it is not true. Asynchrony supports modern Javascript behavior. Let's take a closer look of the asynchronous system and go on the topic of Promise.

What Is Asynchrony in Javascript

First, we need to define the asynchrony in Javascript. I think there are three keys for defining as below.

"The program runs from top to bottom along the written code¹. When the function using external resources (WebAPI, Network, Database) is called², the program will not wait for the return of the function and run next code³."

This behavior is necessary not to idle the CPU. CPU should do other important works such as local computation or rendering while using external resources. So asynchrony improves efficiency although the programming model would be a little complicated.

The program including "setTimeout" function is one of the famous examples running asynchronously. This example is often used because we should call the function using out resources to let the program behave asynchronously and it is very simple.
You know, "setTimeout" function is just an interface and a browser actually counts times.

==Simple Example==

console.log("1");
setTimeout(function() {
    console.log("2");
}, 1000);
console.log("3");

// output
// 1
// 3
// 2
Enter fullscreen mode Exit fullscreen mode

You can understand the result intuitively because console.log("2") runs after 1000(ms) and console.log("3") have already ran before that.

==Counterintuitive Example==

console.log("1");
setTimeout(function() {
    console.log("2");
}, 0);
console.log("3");
// output
// 1
// 3
// 2
Enter fullscreen mode Exit fullscreen mode

The result is same as previous one although console.log(2) waits 0(ms).
We need to understand the back system of calling function to know why this happened.
Loupe helps us a lot to see the flow of calling function.
The overview is something like as below.

Alt Text

The important thing is that callback functions in the queue will not run until the call stack is empty. This is the non-blocking feature.

Generally we can say :

  1. The function is first registered in the call stack
  2. When the function uses external resources, the callback function is registered in the queue
  3. Event loop always monitors stack and if the stack is empty, it places one of the call back functions on the stack (In fact the runtime is multi threading)

What Is The Problem without Promise

Long story short, the problem is "callback hell". If you want to run some asynchronous functions serially, you should write the next processing in the callback function of previous one.
We can easily understand with a simple example.

==Situation==
We would like to read four files (A.txt, B.txt, C.txt, D.txt) whose contents are A,B,C,D respectively and catenate them in order like ABCD.

Alt Text

If you are not familiar with asynchronous functions, this code can be written.

const fs = require("fs");
let all = "";

fs.readFile("A.txt", function (err, data) {
    all += data;
});

fs.readFile("B.txt", function (err, data) {
    all += data;
});

fs.readFile("C.txt", function (err, data) {
    all += data;
});

fs.readFile("D.txt", function (err, data) {
    all += data;
});

setTimeout(function () {
    console.log(all);
}, 100);

// Outputs of some runnings
// ABDC
// ABCD
// ADCB
// ABDC
// ABCD
Enter fullscreen mode Exit fullscreen mode

We can get "ABCD" sometimes, but you cannot get "ABCD" certainly every time. The functions are placed on the call stack in order but the I/O time varies even if it reads same file so that the order of registering the callback function to the queue is different from the one of placing on the call stack.

Alt Text

Now we know that it will work correctly when the callback function is registered in the queue in order. So the way the next computation step is in the previous callback function sounds good.

const fs = require("fs");
let all = "";
fs.readFile("A.txt", function (err, data) {
  all += data;
  fs.readFile("B.txt", function (err, data) {
    all += data;
    fs.readFile("C.txt", function (err, data) {
      all += data;
      fs.readFile("D.txt", function (err, data) {
          all += data;
          console.log(all);
      });
    });
  });
});
// Outputs of some runnings
// ABCD
// ABCD
// ABCD
// ABCD
Enter fullscreen mode Exit fullscreen mode

We can get "ABCD" every time as expected because the code runs repeatedly as below.

Alt Text

We can get "ABCD" every time as expected because the code runs repeatedly as below.

As you can see, the code is nested more deeply if the length of callback function chain gets longer. This is called, as mentioned above, "Callback Hell". It's hard to understand and maintain such a code. Promise solves this problem.

What Is Promise about

It's natural that we want to handle asynchronous behavior just like other functions, which return some objects after processing. Promise enables us this feature.

To be simply put, promise is like a intermediary between javascript world and external resources world. Promise guarantees that it will get the result from external resources in the future. So, you can ask everything about external resources such as return values or error codes to promise, don't you think he is a great partner, man?

Promise has three states.

  • Pending

This is an initial state and promise is waiting for the response from external resources.

  • Fulfilled 

This denotes that promise have already known that external resources succeeded in the process and got some return values from external resources.

  • Rejected  

This denotes that promise have already known that something wrong happened in external resources and got the reason from external resources.

We can see the three states in the simple example.

const fs = require("fs").promises;

// Pendding : Print as soon as promise object is created
let promise1 = fs.readFile("A.txt");
console.log(promise1);

// output
// Promise { <pending> }

// Fullfilled : Print after a second
let promise2 = fs.readFile("A.txt");
setTimeout(function () {
  console.log(promise2);
}, 1000)

// output
// Promise { <Buffer 41> } 
// ↑ Promise has tha returned Buffer object. 0x41 means 'A'.

// Rejected : Read not existing file (E.txt)
let promise3 = fs.readFile("E.txt");
setTimeout(function () {
  console.log(promise3);
}, 1000)

// output
// Promise {
//   <rejected> [Error: ENOENT: no such file or directory, open 'E.txt'] {
//     errno: -2,
//     code: 'ENOENT',
//     syscall: 'open',
//     path: 'E.txt'
//   }
// }
Enter fullscreen mode Exit fullscreen mode

We often use the phrase "if ~, then ~". We can think about Promise like "If the value return from external resources, then do something with it". Anyone doesn't know whether the function will succeeds in processing or fail but we can just write a code for the future. So a promise object prepares "then" function. We write the processing for the future success in "then" function and for the fail in "catch" function. Be careful the fact that the processing in "then" and "catch" will be just registered in the queue and not run immediately.

const fs = require("fs").promises;
let promise = fs.readFile("A.txt");
promise
  .then((data) => {
    console.log(data.toString());
  })
  .catch((err) => {
    console.log(err);
  });

// Generalization
// (Promise Object)
// .then((the returned value) => do something)
// .catch ((the reason of error) => do something)
// .finally(() => do something in both cases )
Enter fullscreen mode Exit fullscreen mode

Promises Chain

We know the fundamental things of promise. But we cannot solve the "ABCD" problem without deep nest right now.
As we can imagine, this code doesn't work.

const fs = require("fs").promises;
let all = "";

fs.readFile("A.txt").then(data => {
  all += data.toString();
});
fs.readFile("B.txt").then(data => {
  all += data.toString();
});
fs.readFile("C.txt").then(data => {
  all += data.toString();
});
fs.readFile("D.txt").then(data => {
  all += data.toString();
});
setTimeout(() => {
  console.log(all);
}, 1000)

// outputs
// ABCD
// ABCD
// ACBD
// CBAD
// BCAD
Enter fullscreen mode Exit fullscreen mode

The file reading functions run in order but the processing in the "then" function will be registered when the I/O finishes so the timing varies every time. This is just a image for helping you understand.

Alt Text

In this situation, promises chain helps us.
Actually, "then" function returns a promise object. When we don't specify the promise object, it returns default undefined promise object. We return the next promise object in the previous "then" function so that the promises can be processed serially.

fs.readFile("A.txt")
  .then((data) => {
    all += data.toString();
    return fs.readFile("B.txt");
  })
  .then((data) => {
    all += data.toString();
    return fs.readFile("C.txt");
  })
  .then((data) => {
    all += data.toString();
    return fs.readFile("D.txt");
  })
  .then((data) => {
    all += data.toString();
    console.log(all);
  });
// outputs
// ABCD
// ABCD
// ABCD
// ABCD
Enter fullscreen mode Exit fullscreen mode

This is the promises chain and it's really easy to read and understand!! And here is a same image as previous.

Alt Text

Other Tips

We can use Promise.all() to solve the "ABCD" problem. Promise.all receives some Promise objects and we can think them as if they were a single Promise object and would return all values at once.

const fs = require("fs").promises;
Promise.all([
  fs.readFile("A.txt"),
  fs.readFile("B.txt"),
  fs.readFile("C.txt"),
  fs.readFile("D.txt"),
]).then((values) => {
  console.log(values);
});
// output
// ABCD
Enter fullscreen mode Exit fullscreen mode

Thank you very much for reading this through to the end!!
Enjoy hacking!!

Top comments (0)