When working with asynchronous operations in JavaScript, we often hear the term Promise. Many people struggle with understanding how Promises work, so in this post I will try to explain them as simply as I can.
To understand this article better, check out my other post about JavaScript Callbacks.
Why JavaScript Promises?
Before we start explaining what a promise is and how it works, we need to take a look at the reason of its existence. In other words, we have to identify the problem that this new feature is trying to solve.
For example, when writing JavaScript, we often have to deal with tasks that rely on other tasks! Let’s say that we want to get an image, compress it, apply a filter, and save it.
Basically we need to follow four tasks:
- Get an image
- Compress it
- Apply filter
- Save it
The very first thing we need to do, is get the image that we want to edit. A getImage
function can take care of this! Only once that image has been loaded successfully, we can pass that value to a resizeImage
function.
When the image has been resized successfully, we want to apply a filter to the image in the applyFilter
function.
After the image has been compressed and we have added a filter, we want to save the image and let the user know that everything worked correctly!
In the end, we will end up with something like this:
getImage('./image.png', (image, err) => { if(err) throw new Error(err) compressImage(image, (compressedImage, err) => { if(err) throw new Error (err) applyFilter(compressedImage, (filteredImage, err)=> { if(err) throw new Error(err) saveImage(compressedImage, (res, err) => { if(err) throw new Error(err) console.log('Successfully saved image!') }) }) }) })
Notice anything here? Although it’s… fine, it’s not great. We end up with many nested callback functions that are dependent on the previous callback function.
This is often referred to as a callback hell, as we end up with tons of nested callback functions that make the code quite difficult to read!
So, in order to recap, the main problems that arise from the use of callbacks are:
- Losing the control of our program execution (Inversion of Control)
- Unreadable code, especially when using multiple nested callbacks
Luckily, we now got something called promises to help us out! Let’s take a look at what promises are, and how they can help us in situations like these!
What is Promise in JavaScript
In JavaScript, a promise is a good way to handle asynchronous operations. It is used to find out if the asynchronous operation is successfully completed or not.
A promise may have one of three states.
- Pending ⏳
- Fulfilled ✅
- Rejected ❌
A promise starts in a pending state. That means the process is not complete. If the operation is successful, the process ends in a fulfilled state. And, if an error occurs, the process ends in a rejected state.
For example, when you request data from the server by using a promise, it will be in a pending state. When the data arrives successfully, it will be in a fulfilled state. If an error occurs, then it will be in a rejected state.
Promises are used to carry out asynchronous tasks like network requests. Using Promises we can write clean and understandable code. Promises were meant to avoid the nesting of callbacks.
Let’s look at an example that will help us understand Promises in a better way.
Create a JavaScript Promise
To create a promise object, we use the Promise()
constructor.
let promise = new Promise(function(resolve, reject){ //do something });
The Promise()
constructor takes a function as an argument. The function also accepts two functions resolve()
and reject()
.
If the promise returns successfully, the resolve()
function is called. And, if an error occurs, the reject()
function is called.
Example of JavaScript Promise
Firstly, we use a constructor to create a Promise object:
const myPromise = new Promise();
It takes two parameters, one for success (resolve
) and one for fail (reject
):
const myPromise = new Promise((resolve, reject) => { // condition });
Finally, there will be a condition. If the condition is met, the Promise will be resolved, otherwise it will be rejected:
const myPromise = new Promise((resolve, reject) => { let condition; if(condition is met) { resolve('Promise is resolved successfully.'); } else { reject('Promise is rejected'); } });
So we have created our first Promise. Kindly note that you will rarely create promise objects in practice. Instead, you will consume promises provided by libraries.
Consuming a Promise: then, catch, finally
Cool! A promise got returned with the value of the parsed data, just like we expected.
But… what now? We don’t care about that entire promise object, we only care about the value of the data! Luckily, there are built-in methods to get a promise’s value. To a promise, we can attach 3 methods:
.then()
: Gets called after a promise resolved..catch()
: Gets called after a promise rejected..finally()
: Always gets called, whether the promise resolved or rejected.
then( ) for resolved Promises:
The then()
method is called after the Promise is resolved. Then we can decide what to do with the resolved Promise. It looks something like this 👇
Let’s make it simpler: it’s similar to giving instructions to someone. You tell someone to ” First do this, then do that, then this other thing, then.., then.., then…” and so on.
- The first task is our original promise.
- The rest of the tasks return our promise once one small bit of work is completed
The syntax of then()
method is:
promiseObject.then(onFulfilled, onRejected);
The then()
method accepts two callback functions: onFulfilled
and onRejected
.
The then()
method calls the onFulfilled()
with a value, if the promise is fulfilled or the onRejected()
with an error if the promise is rejected.
Also note that both onFulfilled
and onRejected
arguments are optional.
For example, let’s log the message to the console that we got from the Promise:
myPromise.then((message) => { console.log(message); });
catch( ) for rejected Promises:
However, the then()
method is only for resolved Promises. What if the Promise fails? Then, we need to use the catch()
method. But first, we need to understand the promise cycle:
Just like then()
, it also returns a promise, but only when our original promise is rejected.
A small reminder here:
then()
works when a promise is resolvedcatch()
works when a promise is rejected
Likewise we attach the then()
method. We can also directly attach the catch()
method right after then()
:
For example,
myPromise.then((message) => { console.log(message); }).catch((message) => { console.log(message); });
JavaScript finally() method:
We can also use the finally()
method with promises. When we want to execute the same piece of code whether the promise is fulfilled or rejected.
For example,
const render = () => { //... }; getUsers() .then((users) => { console.log(users); render(); }) .catch((error) => { console.log(error); render(); });
As you can see, the render()
function call is duplicated in both then()
and catch()
methods.
To remove this duplicate and execute the render()
whether the promise is fulfilled or rejected, we can use the finally()
method, like this:
const render = () => { //... }; getUsers() .then((users) => { console.log(users); }) .catch((error) => { console.log(error); }) .finally(() => { render(); });
Async functions – making promises friendly
The async and the await keyword, added in ECMAScript 2017. These features basically act as syntactic sugar on top of promises, making asynchronous code easier to write and to read afterwards.
The async Keyword:
First of all we have the async
keyword, which we put in front of a function declaration to turn it into an async
function.
An async
function is a function that knows how to expect the possibility of the await
keyword being used to invoke asynchronous code.
The keyword await makes JavaScript wait until a promise settles and returns its result.
They allow us to write promise-based code as if it were synchronous, but without blocking the main thread. They make our asynchronous code less “clever” and more readable.
Before async
/await
, to make a promise we wrote this:
function order(){ return new Promise( (resolve, reject) =>{ // Write code here } ) }
Now using async
/await
, we write one like this:
//👇 the magical keyword async function order() { // Write code here }
But wait……
You need to understand first ->
- How to use the
try
andcatch
keywords then; - How to use the
await
keyword
How to use the Try and Catch keywords
We use the try
keyword to run our code while we use catch
to catch our errors. It’s the same concept we saw when we looked at promises.
Let’s see a comparison. We will see a small demo of the format:
Promises in JS -> resolve or reject
We used resolve
and reject
in promises like this:
function kitchen(){ return new Promise ((resolve, reject)=>{ if(true){ resolve("promise is fulfilled") } else{ reject("error caught here") } }) } kitchen() // run the code .then() // next step .then() // next step .catch() // error caught here .finally() // end of the promise [optional]
Async / Await in JS -> try, catch
When we are using async
/await
, we use this format:
//👇 Magical keyword async function kitchen(){ try{ // Let's create a fake problem await abc; } catch(error){ console.log("abc does not exist", error) } finally{ console.log("Runs code anyways") } } kitchen() // run the code
So here the keyword await
makes JavaScript wait until a promise settles and returns its result.
Now hopefully you understand the difference between promises and async
/ await
.
How to Handle Multiple Promises
Apart from the handler methods (then()
, catch()
, and finally()
), there are six static methods available in the Promise API. The first four methods accept an array of promises and run them in parallel.
- Promise.all
- Promise.any
- Promise.allSettled
- Promise.race
- Promise.resolve
- Promise.reject
Let’s go through each one.
The Promise.all() method
Promise.all([promises])
accepts a collection (for example, an array) of promises as an argument and executes them in parallel.
This method waits for all the promises to resolve and returns the array of promise results. If any of the promises reject or execute to fail due to an error, all other promise results will be ignored.
The Promise.any() method
Promise.any([promises])
– Similar to the all()
method, .any()
also accepts an array of promises to execute them in parallel. This method doesn’t wait for all the promises to resolve. It is done when any one of the promises is settled.
The Promise.allSettled() method
Promise.allSettled([promises])
– This method waits for all promises to settle(resolve/reject) and returns their results as an array of objects.
The results will contain a state (fulfilled/rejected) and value, if fulfilled. In case of rejected status, it will return a reason for the error.
The Promise.race() method
Promise.race([promises])
– It waits for the first (quickest) promise to settle, and returns the result/error accordingly.
The Promise.resolve/reject methods
Promise.resolve(value)
– It resolves a promise with the value passed to it. It is the same as the following:
let promise = new Promise(resolve => resolve(value));
Promise.reject(error)
– It rejects a promise with the error passed to it. It is the same as the following:
let promise = new Promise((resolve, reject) => reject(error));
Conclusion
If you are here and have read through most of the lines above, congratulations! You should now have a better grip of JavaScript Promises.
In short, a Promise is an object that once called upon, will eventually resolve or reject and return a response based on some criteria that is specified within the Promise object.
We hope this post will help you in your journey. Keep learning!
Thanks for reading…😊
Resource
- Methods of Promise: .all(), .any() , .finally(), .race()
- Learn Callbacks, Promises, and Async/Await in JS
- JavaScript Promise and Promise Chaining
- JavaScript Promises – Javascripttutorial
- How to Resolve or Reject Promises in JS
- Handling Concurrency in JavaScript
Add comment