JavaScript Promise

JavaScript Promise

Much like its literal meaning in simple English, a Promise (in JavaScript) represents a commitment that an action will be carried out at a later time.

In this piece, we will explore the basic concepts of the JavaScript Promise object, its importance, and how to to use it in dealing with asynchronous processes.

NOTE: In order to gain true appreciation for the JavaScript Promise object, it is important to have a deep understanding of the concept of synchronous and asynchronous programming.
This article makes use of the ES6 arrow function syntax for function expressions. If you are new to the syntax, please see Enhance Your JavaScript Skills with ES6 Features
.

Introduction

JavaScript is a single-threaded language. This means that its runtime is primarily synchronous—working line after line to execute codes or functions in the same order in which you write or call them.

To demonstrate JavaScript's default synchronous behaviour, consider the simple demo() function:

    const demo = () => {
      console.log(1); //outputs 1 first
      console.log(2); //outputs 2 second
      console.log(3) //outputs 3 third
  }

The demo() function synchronously logs the numbers–1,2,3–to the console, exactly as they appear in the function, and in split seconds. However, when writing codes for real applications, certain functions or processes might take some time to complete.

JavaScript's default synchronous behaviour prevents subsequent processes from executing until the initial process completes. This behaviour can impact performance, and your application might experience occasional glitches and general poor front-end responsiveness.

If you suspect that an operation might take some time to execute, you should declare such operation asynchronously—for instance, fetching data from an API. An asynchronous operation is non-blocking. It allows other actions to be processed simultaneously without obstructing the program flow, only returning a result after it is completed.

Before the Promise object

Callback function was introduced as one of the earliest methods of handling asynchronous operations. While it handled this process with efficiency for the most part where only a few nested callbacks were required, processes requiring multiple nested callbacks led to complicated code structures which were difficult to maintain—something popularly referred to as callback hell or pyramid of doom.
This disadvantage made the callback method of handling asynchronous operations a less desired solution among software developers. It is for this reason that the JavaScript Promise object was introduced.

The Promise object

JavaScript Promise is an object that is designed to effectively handle asynchronous operations. A complete Promise object is made up of two important parts:

  • Producing code: code that takes some time to execute
  • consuming code: code that waits for the result of the producing code

To create a Promise object, use the new Promise() constructor:

     new Promise((resolve, reject) => {
      //producing code goes in here

      resolve(value);//evaluates on success of producing code

      reject(error);//evaluates on error of producing code
    })

The Promise object receives a callback function (as an argument) called an executor, which runs automatically when you create a new Promise.

The executor function contains producing code. It also receives two callback functions as argument: resolve and reject.
The executor calls the resolve function when the producing code successfully returns a value, and the reject function when, it raises an error. The executor can call only one resolve or reject function in a promise because a process cannot succeed and fail at the same time, any further resolve or reject function calls will not run.

The following example demonstrates how to handle an asynchronous operation using Promise:

Example 1

   const myPromise = () =>{
    return new Promise((resolve, reject) => {
      let title = prompt('what is the title of this article');
      setTimeout(()=>{
          if(title.toLowerCase() == 'promise'){
              resolve(`Thank you for paying attention`)
          }else{
              reject(new Error(`please pay more attention!!`))
          }
      },2000)
    })
  }

In the above example, the myPromise() function returns a new Promise object with an executor function that simulates an asynchronous operation using JavaScript's default setTimeOut() method.
If the outcome of the executor function is true or successful, it attaches the result (value) to the resolve callback; if the outcome of the executor function is false or returns an error, it attaches the result (error) to the reject callback.

The promise object property

The promise object has two major internal properties, they are:

  • Promise State
  • Promise Result

These properties exist in the JavaScript engine and are only used to manage the promise behaviour. Therefore, you cannot access them directly through code.

StateResult
pendingundefined
fulfilledvalue
rejectederror

When you call a new Promise, it's state is initially pending, and it's result undefined. The executor function processes automatically and then settles.
If settled successfully, the promise state moves from pending to fulfilled, and it's result from undefined to value; If settled unsuccessfully, the promise state moves from pending to rejected and it's result from undefined to error.

How to consume the Promise object

A promise object is without meaning and serves no purpose if you do not use it's result in some way.
After the executor function settles, you have to decide what to do with the result (value or error) returned by the producing code. This process is popularly referred to as consuming the promise.

The two methods used for consuming a promise are .then() and .catch().

Promise .then()

The .then() method receives two callback functions as arguments.

    Promise.then(value => {
      doSomething(value) //value handling logic
    }, error => {
      doSomething(error) //error handling logic
    } )

The first callback function runs if the promise is successful, and it's argument is the value it receives from the resolve callback in the producing code; the second function runs if the promise is unsuccessful, and it's argument is the error it receives from the reject callback in the producing code.
Only one of the callback functions is run depending on the result returned by the executor function of the producing code, while it ignores the other.

The code below describes how to consume the promise that the executor returns from the myPromise() function previously declared in Example 1:

Example 2

  myPromise().then(value=>alert(value), err=>console.error(err));

Running this code prompts you to input the title of this article into a pop-up input field. A hypothetical scenario will have the executor scanning through a database to authenticate your response—this is an action that might take some time as simulated using the setTimeOut() method.

If your answer is correct after the promise settles, it's result (value)—attached to the resolvecallback—passes to the first function of the .then() method which alerts the value on your screen. If it is wrong, it's result (error)—attached to the rejectcallback—passes to the second function of the .then() method which then logs the error message to the console.

If you're only interested in consuming the value of a successful promise, you can skip the error callback in the .then() method:

Example 3

  Promise.then(value => {
    doSomething(value) //value handling logic
  } )

Using the myPromise() function previously declared in Example 1:

myPromise().then(value=>alert(value) //alerts 'Thank you for paying attention')

But if you're interested in consuming only the value of a failed promise, replace the success callback with null and proceed with your error handling logic:

Example 4

  Promise.then(null, error => {
    doSomething(error) //error handling logic
  } )

Using the myPromise() function previously declared in Example 1:

myPromise().then(null, err=>console.error(err) //logs 'please pay more attention!!')

Promise.catch()

Unlike the .then() method, the .catch() method consumes only rejected promise. This method offers a simple and concise alternative to the .then() method of handling rejected promise by omitting the null value.

Example 5

  Promise.catch(error => {
    //error handling logic
  } )

Using the myPromise() function previously declared in Example 1:

myPromise().catch(err=>console.error(err) //logs 'please pay more attention!!')

Promise chaining

When dealing with a promise, it is often the case that you may not arrive at your final result after the initial consuming code. In this case, the initial consuming code returns a new promise, which you then have to link to one or more subsequent consuming codes using multiple .then() methods. This process is referred to as promise chaining.

The following examples describe how to implement the promise chaining technique:

NOTE: Always include the return keyword when attaching a new promise operation to the promise chain, so that it's value can be passed onto the next chain for consumption.

Example 6: Alerting random numbers

  const randNum1 = () =>{
    return new Promise(resolve =>{
        setTimeout(() => {
            resolve(Math.floor(Math.random()*10))
        },500)
    })
}

  const randNum2 = () =>{
      return new Promise(resolve =>{
          setTimeout(() => {
              resolve(Math.floor(Math.random()*10))
          },1000)
      })
  }

  const randNum3 = () =>{
      return new Promise(resolve =>{
          setTimeout(() => {
              resolve(Math.floor(Math.random()*10))
          },1500)
      })
  }

  randNum1().then(result=>{
      alert(`First Number: ${result}`)
      return randNum2()
  }).then(result=>{
      alert(`Second Number: ${result}`)
      return randNum3()
  }).then(result=>{
      alert(`Third Number: ${result}`)
  })

Example 7: Fetching data from an API

Fetching data from APIs is one of the common functionalities of most web applications. Promise chaining enables you to manipulate and consume fetched data easily.

    const URL = 'https://jsonplaceholder.typicode.com/todos'
    fetch(URL)
      .then(response => {
        //block1
        if(!response.ok) {
          throw new Error('Error generating response')
        }
        return response.json()
      })
      .then(data => console.log(data)//block 2)
      .catch(error => console.log(error)//block 3)

The fetch() function in the code above is an in-built JavaScript function that makes asynchronous network requests to a server.

Here is an explanation of what the code is doing:

  1. The fetch() function makes a request to the URL and returns a promise that resolves to a Response object.

  2. The first .then() block (block 1) accesses the response status and handles it asynchronously.
    If the response.ok value is false, it triggers the custom error message specified; if the response.ok value is true, it parses the json data and returns its value to the next .then() block (block2).

  3. The second .then() block (block 2) receives the data from block 1 and logs it to the console.

  4. The .catch() block handles any error that occurs during the entire data-fetching lifecycle.

Promise.finally()

This promise method allows you to execute callback functions that run when a promise is settled, regardless of it's outcome (resolved or rejected). The callback function passed to .finally() does not receive any arguments and cannot modify the state or value of the promise.

  const URL = 'https://jsonplaceholder.typicode.com/todos'
    fetch(URL)
      .then(response => {
        //block1
        if(!response.ok) {
          throw new Error('Error generating response')
        }
        return response.json()
      })
      .then(data => console.log(data)//block 2)
      .catch(error => console.log(error)//block 3)
      .finally(() => alert('data fetch completed'))

Conclusion

The JavaScript Promise object has become widely embraced among software developers.
Many modern web applications now use it to manage asynchronous processes, optimize performance, and improve user experience. Therefore, it is important that you understand the workings of these processes and how to handle them.

Mastery of the promise object and its proper implementation will help you create very powerful applications that are responsive to user interactions, deliver a seamless user experience, and execute other tasks in the background—all at the same time.

Understanding the promise object can prove to be challenging for first-timers, so feel free to revisit this article as often as necessary.