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
orFulfilled
orRejected
, 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 fromundefined
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 thevalue
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 thevalue
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
tofulfilled
then we need to call theresolve
function. whatever value is allocated to the argument of the resolve function will get thevalue
property of promises.
- if you want your promise state to change from
if you want the future signal to be
rejected
- if you want your promise state to change from
pending
torejected
then we need to call thereject
function. whatever value is allocated to the argument of the reject function will get thevalue
property of promises.
- if you want your promise state to change from
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
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