DEV Community

Bolaji Bolajoko
Bolaji Bolajoko

Posted on

JavaScript Promises

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
});
Enter fullscreen mode Exit fullscreen mode

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 and reject 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.

Promise Object Properties Image

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);
Enter fullscreen mode Exit fullscreen mode

Output

Pending state image

This loads after three seconds:

Fulfilled state image

// Changing the counter value to false
const counter = false;
Enter fullscreen mode Exit fullscreen mode

Output:

Failed state

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Output:

Even number 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, the then() help us to resolve or reject 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))
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode
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
});
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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);
})
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

Output:

Weather data 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

  1. Handle both resolved and rejected states: Always handle both the resolved onfulfilled and rejected onRejected states of a Promise. Use the then() method to handle the resolved and catch() method to handle the rejected state.

  2. 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 use finally() method regardless of what the the outcome might be. finally() method should not a return a statement.

  3. 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 using new Promise constructor. This helps to simplify code and reduces the risk of introducing complexity and mistakes.

  4. Handle error with catch(): To handle error in asynchronous operation, use the catch() to handle any rejected Promises. This allows you to centralize error handling logic in a single location and avoid repetitive error handling code.

  5. 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() and catch() 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)

Collapse
 
sharmi2020 profile image
Sharmila kannan

Wonderful explanation, got detailed info about the promises

Collapse
 
seven profile image
Caleb O.

This is a nice breakdown. Thank you for sharing it!

Collapse
 
bolajibolajoko51 profile image
Bolaji Bolajoko

You welcome Caleb

Collapse
 
volodyslav profile image
Volodyslav

Gonna practice with my city 😁 thanks a lot!

Collapse
 
bolajibolajoko51 profile image
Bolaji Bolajoko

Alright... that's will be fun.😊

Collapse
 
dcodeh profile image
d-code-h

Nice article! Really educating. Thanks Bolaji

Collapse
 
bolajibolajoko51 profile image
Bolaji Bolajoko

Thanks to you, for taking your time to read the article.