Introduction
There are various ways asynchronous operation can be handled or managed, major ones include the use of Callbacks, Promises and Async/Await. In this article, I will be discussing on how you can use JavaScript Promises to handle an asynchronous operations.
JavaScript Promises are an important feature in JavaScript that is used to handle asynchronous operations or tasks that will take some time to complete, such as network requests, database queries or file operations.
JavaScript Promise is one of the best approaches used to handle asynchronous operations in modern JavaScript development because it improves error handling, flow control and readability.
What is JavaScript Promise
Analogy: Let's say your friend has promised to take you out for dinner on a weekend for the way he spoke to you at work. He arranged the venue and time for meetup. There are two things that might happen for the dinner to hold: either your friend makes it to the venue by alerting you, or your friend sends you a message that he couldn't make it with reasons. JavaScript Promise is similar to this scenario.
JavaScript Promise is an object that represents the completion or settlement of an asynchronous task.
Creating Promise
To create a promise object, you can use the constructor syntax.
const myPromise = new Promise((resolve, reject){
// some operation
});
The callback function passed into the constructor is called an executor, which is typically used to perform the asynchronous operation. The executor function has two callback functions as the argument: the resolve()
and reject()
callback functions, which are provided by JavaScript.
The
resolve
andreject
are built-in functions provided by JavaScript when creating a Promise.
If an asynchronous operation runs successfully, the executor will call the resolve
method and return a value.
In case the asynchronous task encounters an issue (error), the executor function will call the reject
method and return an error with a reason.
The Promise object returned by the constructor has two internal properties, state and result.
When a new promise is created, its state is pending
with a result of undefined
. When the asynchronous operation is successful, the state is changed to fulfilled
with a result of value
, and when an error is encountered, the state is changed to rejected
with a result of error
.
A promise can only move from a pending
state to either being fulfilled
or rejected
. Once a promise is fulfilled
or rejected
, it cannot go back to another state.
If a promise reaches a fulfilled
or rejected
state, the asynchronous operation is said to be resolved.
Let's look at a simple example to show the state and result properties of a Promise object.
const counter = true;
const myPromise = new Promise((resolve, reject) => {
// using setTimeout as an async operation
setTimeout(() => {
if (counter){
resolve("Counting...");
}
else {
reject(new Error("Can't Count"));
}
}, 3000);
});
console.log(myPromise);
Output
This loads after three seconds:
// Changing the counter value to false
const counter = false;
Output:
Promise Methods
Most of the time you won't be using the resolve()
and reject()
methods to handle asynchronous task. JavaScript provides some Promise methods you can use to handle the resolving and rejection of operations.
We will be taking a look at some of the most important fundamental Promise methods JavaScript provides in the subsequent section of this article, such as then(), catch(), and finally()
then()
then()
method is one of the core methods of Promise object. It specify what should be done when an asynchronous task succeeds (fulfilled) or fails (rejected).
The main purpose of then()
is to attach the rejection, and fulfilment handlers to a Promise. Its takes two callback methods, one for fulfilment and the other one for rejection.
Syntax
promise.then(onFulfilled, onRejected);
onFulfilled
- this is a function that runs when the promise is resolved and returns a result.
onRejected
- this is a function that runs when the promise is rejected and returns a reason for the rejection.
then()
will call the onFulfilled()
if the Promise is resolved successfully or the then()
will call the onRejected()
if the Promise is rejected.
// Resolving Promise
const pResolved = new Promise((resolve, reject) =>{
setTimeout(() => resolve("Successful!"), 1000);
})
pResolved.then(result => console.log(result),
error => console.log(error));
// Output: Successful!
//Rejecting Promise
const pRejected = new Promise((resolve, reject) =>{
setTimeout(() => reject("Error"), 1000);
});
pRejected.then(result => console.log(result),
error => console.log(error));
// Output: Error
In the example above, the resolve()
in the setTimeout()
signifies that the Promise is resolved and will call the necessary argument in the then()
, which is onFulfiled()
, the first argument in the then()
method.
In the second part of the code, the reject()
method in the setTimeout()
method signifies that the promise is rejected, thereby calling the second argument in the then()
method, which is onRejected()
The then()
method can accepts either the onFulfiled()
or onRejected()
. Its depend on which promise state we are more interested in.
Putting it together:
Now, let's put what we've been discussing into practice by writing some code. You will be building an even numbers generator system. The system will raise an error when encountering an odd number.
function getEven(){
const myPromise = new Promise((resolve, reject) =>{
setTimeout(()=>{
const rand = Math.floor(Math.random() * 10);
if (rand % 2 == 0){
resolve("Even Number - " + rand);
}
else {
reject(new Error("Invalid " + rand))
}
}, 1000);
});
myPromise.then(result => console.log(result),
error => console.log(error));
}
setInterval(getEven, 2000);
Output:
The code example demonstrates the creation of a getEven
function that generates random even numbers asynchronously. It utilizes Promises, setTimeout
to simulate the asynchronous behavior. The Math.random
in the code is used to generate numbers. The then()
method handles the resolving or rejection of the Promise, allowing for the logging of valid even numbers or error messages. The function is executed every two seconds using setInterval()
.
Key takeaway from using the
then
method, thethen()
help us toresolve
orreject
an asynchronous task.
catch()
Handling Promise rejection can be done in different ways. One approach is to use the then()
method's second argument onRejected()
to handle errors. Another option is to use the catch()
method, which can be chained to the Promise object. The catch()
method accepts a callback function to handle the error.
Syntax
promise.catch(callback(error))
const run = false;
const myPromise = new Promise((resolve, reject) => {
setTimeout(()=>{
if (run){
resolve("Running...")
}
else{
reject(new Error("Error"));
}
}, 1000)
});
myPromise.catch(error => console.log(error)); // Output: Error
In the example the else
statement is executed, thereby executing the catch()
method handler on the Promise object, the Promise failed and the reason for it failure is stated.
If the run
variable value is changed to true
the if
statement will be executed and the promise will be resolve
which will make the catch()
handler not to be executed.
The
catch()
handler will only be executed if the Promise is rejected or failed.
finally()
Yes! finally, but that's not the end of this article 😃, we still have to clean up our code mess and discuss some few concepts. Let's do some cleanup using finally()
method.
We might want to execute some functions whether the Promise gets resolved or not. This is where finally()
method comes into play. The finally()
method of a Promise object assigns a function to be called when a Promise is settled (either fulfilled or rejected).
The finally()
method is primarily used to ensure the execution of code that should always run, regardless of the result.
Syntax
promise.finally(doSomething());
If a
finally()
handler return something, it will be ignored.
const counter = true;
const myPromise = new Promise((resolve, reject) =>{
setTimeout(() =>{
if (counter){
resolve("Completed!")
}
else {
reject("Failed")
}
}, 1000);
});
myPromise.then(result => console.log(result))
.finally(() =>{
console.log("Do Something!")
});
// Output: Completed!
// Output: Do Something!
In the example above, the Promise is resolved and the finally()
handler is attached to then()
handler, and the finally()
gets executed. The process of attaching a Promise handler with another handler is called PROMISE CHAINING.
Promise Chaining
We might be in a situation where we need a result of an asynchronous operation in other another one. With the Promise API, we can pass down return result from one handler to another. This method is called PROMISE CHAINING.
When passing down the return result the next handler pick up the result first before moving forward.
Syntax:
myPromise.then()
.then()
.then();
const myPromise = new Promise((resolve, reject) =>{
setTimeout( resolve(1), 1000)
})
myPromise.then(result => {
console.log(result); // 1
return result;
})
.then((result) =>{
return result * 2
})
.then((result) =>{
console.log(result); // 2
return result * 2;
})
.then((result) =>{
console.log(result); // 4
return result * 4;
})
.then((result) =>{
console.log(result); // 16
});
In the example above the return result trickles down the chain and gets used by the next handler in the chain.
Error Handling in a Promise Chain
When using Promise Chaining a good practice is to handle errors that might occur during promise execution. The most prominent method used for handling chained promise errors is to attach a catch()
handler to the end the Promise chain.
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2)
});
});
myPpromise.then((result) => {
console.log(result); // 2
return result * 2;
})
.then((result) => {
console.log(result); // 4
return result * 4;
})
.then((result) => {
console.log(result); // 16
return result * 5;
})
.then((result) => {
console.log(result); // 80
})
.catch((error) => {
console.log(error); // This handles any error in the chain
}
In the example above, if an error is encountered in the promise chain the catch
block will get implemented, thereby raising an error with a reason. For instance if an error is encountered in the second then()
block, the error will be caught by the catch()
and the rest of the then()
will not be executed.
We might want to handle an error in a specific block and still maintain the results of a Promise chain. The effective approach is to wrap the specific instruction in a try-catch block. By doing so, we handle the error locally without breaking the flow of the Promise chain.
const myPromise = new Promise((resolve, reject) =>{
setTimeout(() => {
resolve(2);
})
});
myPromise.then((result) => {
console.log(result); // 2
return result * 2;
})
.then((result) => {
console.log(result); // 4
try {
if (result === 4){
throw new Error("Oops! Something went wrong here")
}
}
catch(error){
console.log(error); // Oops! Something went wrong here
}
return result * 2;
})
.then((result) => {
console.log(result); // 8
return result * 2;
})
.then((result) => {
console.log(result); // 16
})
.catch((error) => {
throw new Error(error);
})
In the example above a try-catch block is used to handle the error that might occur in the second code block and we can still maintain the flow control of the returned result down the chain.
Putting it together
In this section we will be implementing what we've learned so far. We will be fetching weather data from a weather API and we will be using Promise to handle the asynchronous operation.
On this task, we are going to be making use of a weather API to request for the current weather details of a specific city. You can check their official website Weather API and sign-up to obtain your API key.
function getData(city){
const api_key = "your api key";
const url = `https://api.weatherapi.com/v1/
current.json?key=${api_key}&q=${city}`;
fetch(url)
.then(response => {
if (!response.ok){
throw new Error("Error: " + response.status);
}
return response.json();
});
.then(data => {
console.log(data);
});
.catch(error => console.log(error));
}
getData("New York");
Output:
In the code sample above, the getData
function takes a city
parameter input, then we construct the API URL by combining the Weather API endpoint with an API key and a specified city
. The fetch
function is used to send an HTTP GET request to the constructed URL, the fetch
function returns a Promise, which is chained with .then()
to handle the response. In the first then()
method we check if the response is successful using the response.ok
property. If the response is not successful, it throws an error with a status code. The response proceeds with the result to the next then()
if the response is successful. In the second then()
block, it parses the response using response.json
to obtain the weather data, then it logs the retrieved weather data to the console. If any errors occur during the fetch()
method request or data processing, they are caught and logged in the catch()
callback. To use the getData
function and fetch the data for a specific city, you call the getData
and passing in the name of a city.
Promises best practices and tips
Handle both resolved and rejected states: Always handle both the resolved
onfulfilled
and rejectedonRejected
states of a Promise. Use thethen()
method to handle the resolved andcatch()
method to handle the rejected state.Cleanup using
finally()
: You might want to perform some cleanup operation such as closing connections, or releasing some resources regardless of whether the Promise is fulfilled or rejected. You can usefinally()
method regardless of what the the outcome might be.finally()
method should not a return a statement.Avoid using Promise constructor unnecessarily: In most cases it is recommended to use Promise-based API's (e.g.,
fetch
,setTimeout
), Instead of manually constructing a Promise usingnew Promise
constructor. This helps to simplify code and reduces the risk of introducing complexity and mistakes.Handle error with
catch()
: To handle error in asynchronous operation, use thecatch()
to handle any rejected Promises. This allows you to centralize error handling logic in a single location and avoid repetitive error handling code.Remember to return Promises: When creating a custom function that performs an asynchronous operation that returns a Promise, make sure to return the Promise so that the callers of the function can chain
then()
andcatch()
as needed.
Conclusion
By understanding Promises and their various methods, developers can write more efficient and maintainable code when working with asynchronous tasks.
Top comments (7)
Wonderful explanation, got detailed info about the promises
This is a nice breakdown. Thank you for sharing it!
You welcome Caleb
Gonna practice with my city 😁 thanks a lot!
Alright... that's will be fun.😊
Nice article! Really educating. Thanks Bolaji
Thanks to you, for taking your time to read the article.