Unleashing the Power of Promises in JavaScript: A Beginner's Guide

Promises are indeed one of the most essential concepts in JavaScript. They revolutionized the way asynchronous operations are handled in the language and significantly improved the readability, maintainability, and error handling of asynchronous code.

Before the introduction of Promises, managing asynchronous operations relied heavily on callbacks, which often led to complex and nested code structures. This approach, known as "callback hell," made code difficult to understand, debug, and maintain. Promises provide a cleaner and more structured solution to asynchronous programming.

Promises are native to javascript, you can find this in the official javascript documentation (promises-link).

From the official documentation of the javascript, we learned that promises are objects with some special capabilities. Promises object act as the placeholder for the data which we hope to get back in the future.

A promise object has the following properties:

  • State: every promise object has a state which can be either Pending or Fulfilled or Rejected , let's discuss these three stages.

    • Pending: In the initial stage when execution is taking place then the state of the promise object is pending. It is also the default state of the promise object.

    • Fulfilled: If the task in the future completes successfully, then the state resolves to fulfilled it.

    • Rejected: If the task in the future doesn't complete successfully, then the state resolves to Rejected.

  • Values: every promise object contains some value, during the initial stage when the execution is taking place till then the value is undefined .When the state of the promise changes to fulfilled or rejected then the value changes from undefined to a particular data.

  • onFulfillment: Basically, it is an array which stores the callbacks, it gets triggered when the state of the promise changes to fulfilled .All of these callback functions inside the array take one argument which is the value property of the same Promise. executed using .then() method.

  • onRejection: Similarly, it is also an array which stores the callbacks, it gets triggered when the state of the promise changes to reject .All of these callback functions inside the array take one argument which is the value property of the same Promise. executed using .then() method.

Note: Once the promise changes its state from pending to fulfilled or rejected , then it states can't be changed again.

How to Create a Promise Object?

To create the promise object in javascript we can use promise class constructor. new Promise constructor is used for creating the promise object.

The executor function is executed immediately when the Promise is created and receives two parameters: resolve and reject. By calling either resolve or reject, you can control the fate of the Promise, whether it should be fulfilled or rejected.

let p = new Promise(function executor(reslove,reject){
           //executor callback
})

If the above executor function is executed then in the output <pending> is displayed which give us the detail of the pending state.

in the above image, you can see that PromiseResult attribute on expanding the <pending> state displaying the value property of the promise (as discussed above)

Now if you remember, I said that once the task is done state of the promise changes. But it is still not changing. Why is that?

As we didn't write the executor callback in the above code so firstly we have to write the executor callback. Now JS will not give any special permissions to the promise object as it is a native feature, so the execution of code inside the executor callback (until async operation powered by runtime comes up) is always synchronous.

let p = new Promise(function executor(reslove,reject){
            for(let i = 0; i < 10000000000; i++) {
                // sync piece of code 
    }
});

In the above piece of code, you can see that there is a block for loop due to this <pending> state of the promise object takes time to display execute synchronously.

let p = new Promise(function executor (res,rej){
         let a = 1;
         setTimeout(()=>{
            console.log("Timer done")
         },2000)
         a++;
         console.log(a);
});

In the above piece of code, no blocking piece of code, every line is just a constant time operation and even the above code have a timer, but the timer will run in the background there will be no halt. This code will be executed asynchronously.

Below is the output.......

Now, we write the exec for the future, which will decide whether our promise object is fulfilled or rejected

  • if you want the future signal to be fulfilled

    • if you want your promise state to change from pending to fulfilled then we need to call the resolve function. whatever value is allocated to the argument of the resolve function will get the value property of promises.
  • if you want the future signal to be rejected

    • if you want your promise state to change from pending to rejected then we need to call the reject function. whatever value is allocated to the argument of the reject function will get the value property of promises.
const myPromise = new Promise(function exec(resolve, reject){
  console.log("inside exec");
  setTimeout(function f(){
    resolve("foo");
  }, 5000);
console.log("Timer is running")
});

In the above code, you can see how the pending states of mypromise changes from <pending> to fulfilled .Now let's analyse how the state has changed of the above code, firstly we enter in the exec function created in the promise constructor "inside exec" is printed on the console then we get in timer which starts the timer in the background of 5sec we move on "Timer is running" is printed until this state is pending when the timer is completed then resolve function is executed which then finally changes the state from pending to fulfilled . value property is updated to foo from default undefined.

After 5 sec as per the timer state changes

const myPromise = new Promise(function exec(resolve, reject){
  console.log("inside exec");
  setTimeout(function f() {
    reject("foo");
  }, 5000);
console.log("Timer is running")
});

In this function instead of moving from Pending to fulfilled it got rejected this time, but the same thing will happen, the state will change for the promise object and the value property of the promise object is foo.

Note: As said earlier once the state changes from pending to fulfilled or rejected state it can't be reverted, if once the state change then there is no meaning in further try to change the state i.e. once resolve no chance of further resolve or reject .

Promises Unleashed: The Dynamics of State Changes

When a Promise state changes from pending to fulfilled or rejected, whatever function we call resolved or rejected gets the value property of promises and it can't be changed. Now, the callbacks function which are in onfulillment or onRejection array transfers to the micro-task queue.

As we know what is a macro-task queue is? when the async function completes, then it has to wait inside the macro-task queue.Promise on fulfilment or rejection, perform various callback which is stored in onFulfillment or onRejection array.

You might be thinking, why do these callbacks (of promises) have to go to the Microtask queue? Why they cannot be immediately executed?

Because Promise fulfilment will happen sometime in future due to some async tasks. If we have some code running on our main thread, and in between the promise fulfils, then JS will never hamper the flow of the main thread and will not execute these callback functions immediately hence they have to wait in the Micro task queue.

Now one more question might come to your mind, why Promise based callbacks have to wait in the Micro task queue and not in the Macro task queue? Because JS wants to set the priority of the callback executions, the priority of callbacks waiting in the Microtask queue is always and always higher than callbacks waiting in the Macrotask Queue.

So if at any point in time, there is a callback waiting in the Macrotask queue and another callback waiting in the Microtask queue, then the one in the Microtask queue will be executed first.

Now, How does the Micro-task queue execution take place by EVENT LOOP? Same as the Macro-task queue.

Event loop checks first if the callstack is empty, if yes then global code is executed completely if yes, then checks callback in the Micro-task, executes callbacks in the micro-task queue on priority when it is implemented then only the Macro-task queue is executed.

Consumption of Promises

Every function using newPromise constructor contains .then function which makes the execution of a future function onfulfillment or onRejection .

.then function takes two arguments, first is fulfillhandlerCallback and other is rejectionhandlerCallback .

p.then(function fulfillhandler(){},function rejectionhandler(){});
//p is a promise object

Once p.then function is executed only one thing happens fulfillhandler and rejectionhandler are pushed to the onFulfillment or onRejection array. Keep in mind, when you do .then then these callbacks are not executed, they are just pushed (or you can say registered) in their corresponding arrays.

multiple .then functions are allowed i.e. you can have more than one .then functions

 p.then(function fulfillhandler1(){},function rejectionhandler1(){});
 p.then(function fulfillhandler2(){},function rejectionhandler2(){});
 p.then(function fulfillhandler3(){},function rejectionhandler3(){});

from the above code, three functions will be pushed in the onFulfillment and onRejection array for execution.

Now these callbacks (i.e. fulfil and reject callbacks) can also take one argument. This argument is the value property of the Promise object.

we can also write only fulfillhandler , alone can also be pushed without rejectionhandler. If there is only one callback in the .then function then that will be fulfillhandler.

let p = new Promise(function exec(resolve, reject) {
    console.log("inside exec");
    let a = 30;
    console.log("Started the timer");
    setTimeout(function f() {
        a += 10;
        resolve(a);
        console.log("Timer done");
    }, 10000);
    console.log("Timer is running");
    a += 5;
});
p.then(function f1(v) { console.log("fulfill handler 1", v); },
       function r1(v) { console.log("reject handler 1", v); });

p.then(function f2(v) { console.log("fulfill handler 2", v); },                      
       function r2(v) { console.log("reject handler 2", v); });

So the current state of execution is that we have called the Promise constructor, and inside it, we are calling the executor callback. Inside the executor callback, we initiated a timer of 10s after whose completion we should execute the callback function f. JS will just trigger the timer and comes back. "inside exec" and "Started the timer" are printed

Exec is executed remove from the callstack, promise object p is created, contains all the promise properties. current state is pending ,value is undefined and both onfulfillment onrejection arrays are empty.

promise constructor is executed, Now we move to p.then function where onfulfillment and onrejection array will be pushed by {f1,f2} and {r1,r2} respectively.

And by this timer is still going on.

Now let's say timer is completed after 10s.

The moment timer is completed it will go to the macro task queue, meanwhile event loop is constantly in the call stack empty. YES. Is the global piece of code all done? YES. Is the Microtask queue empty? YES.

Now event loop will bring the callback function f from the Macro task queue to the call stack. In the function f we increment the value of a and then call the resolve function with the value a (which is 45)

So now, the state of the promise changed to fulfilled, the value changed to 45 and it immediately f1 and f2 callbacks are sent from onFulfilled array to Microtask queue. But these callbacks cannot be immediately executed. Why? Because the event loop will check if the call stack empty? No. The function f is still going on as there is still one line of code left i.e. console.log("Timer done").

The moment function f completes. event loop will check if the call stack is empty. yes. Is the global code done? Yes. So Event loop will check if the Microtask queue is empty? No.

First of all, f1 will be called and executed.

Then f2 will be called and executed.

When both of them are done, the Microtask queue will be empty, and there is nothing in the Macrotask queue, call stack or global code, so the Program ends.

What is Promise.resolve?

Promise.resolve() is a static method in JavaScript's Promise API that creates a new Promise object that is immediately resolved with the provided value. It is commonly used when you want to create a Promise that represents a successful or fulfilled state.

Here's an example of using Promise.resolve() with a detailed explanation:

const resolvedPromise = Promise.resolve("Success!");

resolvedPromise.then((value) => {
  console.log(value); // Output: Success!
});

In the above example, we create a new Promise object called resolvedPromise using Promise.resolve("Success!"). The value "Success!" is passed as the argument to, indicating that the Promise should be resolved with this value.

We then attach a then() method resolvedPromise to handle its fulfillment. The then() the method takes a callback function that is executed when the Promise is fulfilled. In this case, the callback function receives the fulfillment value as an argument, which is "Success!". We log this value to the console, resulting in the output "Success!".

The Promise.resolve() method is useful in scenarios where you want to create a Promise that is immediately fulfilled with a specific value, without performing any asynchronous operations. It simplifies the creation of Promises and allows you to seamlessly integrate synchronous and asynchronous code.

// Example 2: Wrapping an asynchronous operation
const fetchData = () => {
  // Simulating an asynchronous operation
  return new Promise((resolve) => {
    setTimeout(() => {
      const data = { id: 1, name: "John Doe" };
      resolve(data);
    }, 2000);
  });
};

Promise.resolve(fetchData()).then((result) => {
  console.log("Fetched data:", result);
});

In this example, we have an asynchronous operation fetchData() that returns a Promise representing the fetching of data. Instead of directly calling, we wrap it within Promise.resolve(). This converts the Promise returned fetchData() into a new Promise that immediately fulfils with the same value. By chaining then() on Promise.resolve(), we can handle the fulfillment of the resolved Promise and log the fetched data to the console.

The Promise.resolve() method is versatile and can be used in various scenarios. It allows you to convert values, wrap non-Promise values, or handle the immediate resolution of Promises. It's a convenient tool for working with Promises and integrating them with other parts of your code that may not directly return Promises.

what is .then function ?

The .then() function is a fundamental method in JavaScript Promises that allows you to handle the fulfillment or rejection of a Promise. It takes two optional callback functions as arguments: the onFulfilled callback and the onRejected callback. Here's a detailed explanation of the .then() function with more examples:

every .then returns a new Promise object. Now the .then function also returns a promise, and it will be fulfilled with undefined if you will not return something manually from it.

x = Promise.resolve(7);
console.log(x);
y = x.then((v) => {console.log(v)})
console.log("what is .then returning ? ", y);

So in the above code, we are not returning anything manually from the fulfillHandler of .then so we get undefined.

x = Promise.resolve(7);
console.log(x);
y = x.then((v) => {console.log(v); return 100;})
console.log("what is .then returning ? ", y);

How did promises help to resolve the inversion of control?

Promises play a significant role in resolving the inversion of control, also known as the "callback hell" problem, in JavaScript. In traditional callback-based asynchronous programming, the control flow is often passed from the caller to the callee through callback functions. This can lead to deeply nested and difficult-to-read code structures, making it hard to reason about the flow of execution.

Now here, we are not passing our callbacks to other functions, we are not giving those functions control over us. We are keeping our callbacks at our call site, so we are damn sure they will be called once.

function fakeDownloader() {
    return new Promise((res, rej) => {
        setTimeout(() => {
            res("data");
        }, 4000);
    });
}

Now I am not passing any callback here, in a callback-based code, we need to pass a callback to execute something after downloading is over. But as we have Promises, we don't need callbacks.

let p = fakeDownloader();
p.then(function f(data){
    console.log("downloaded data is", data)
});

The fulfillHandler of p.then is expected to be executed once the download is done, and we can see the control is with us.

If this same functionality was written with a callback it would look something like this:

function fakeDownloader(cb) {
  setTimeout(() => {
     cb("data");
  }, 4000);
}
fakeDownloader((data) => {
    console.log("downloaded data is", data)
});

But we are not sure how they are handling the callback, as they might call it more than once, or maybe never. But with promises, even if somebody tries to call res("data") more than once, the state of the promise changes only once, hence callback will be called only once.

function fakeDownloader() {
    return new Promise((res, rej) => {
        setTimeout(() => {
            res("data");res("data");res("data");
        }, 4000);
    });
}
let p = fakeDownloader();
p.then((data) => {
    console.log("downloaded data is", data)
});

above code called once only......

function fakeDownloader(cb) {
  setTimeout(() => {
     cb("data");cb("data");cb("data");
  }, 4000);
}
fakeDownloader((data) => {
    console.log("downloaded data is", data)
});

The above code will call a callback thrice.

In callbacks, you are giving control of your function to another function in which it is decided where to call your callback and when to call your callback.

But with promise, you have only control of your function.

So this is how inversion of control is resolved.

What about callback hell?

You can already see that the promise-based implementation is cleaner and better understood. It doesn't create a pyramid-like structure.

There is a concept of Promise hell as well, where people start writing nested Promises. This can be easily avoided by using a .then chaining or async await.

Thank you for joining us on this exploration of Promises in JavaScript. We hope this article has provided you with a comprehensive understanding of Promises and how they contribute to more readable and maintainable asynchronous code. Embracing Promises can greatly enhance your programming experience and help you write more efficient and elegant JavaScript applications. Happy coding!

.

.

.

AMANDEEP SINGH

7
Subscribe to my newsletter

Read articles from Amandeep Singh Narang directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Amandeep Singh Narang
Amandeep Singh Narang