Mastering Javascript Promises
saroj sasmal
Posted on May 8, 2021
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);
})
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');
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);
}
});
}
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
});
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);
}
});
}
});
}
});
}
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))
})
}
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));
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));
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);
})
}
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
*/
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
*/
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);
});
});
}
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));
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));
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))
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"
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"))
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;
});
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);
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.
Posted on May 8, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 27, 2024
November 6, 2024