Introduction to Promises

At times while coding, we have operations that take an unknown amount of time to run and give back a result. Promises are a good way of handling such operations(asynchronous operations). For example, fetching data from a server using an API. Fetching this data may take time and so the result will not come back immediately.

A promise is a javascript object which produces a value after an asynchronous operation executes successfully. If the operation does not execute successfully, then it generates an error.

It is used to link producing code and consuming code. Producing code is code that performs a task and produces the result. Consuming code is the code that consumes the result. It does not run until it receives the result from the producing code.

To construct a promise, we use a constructor as shown in the code snippet below:

//Pseudocode
//The 'p' in 'new promise' should be capitalized.
let promise = new Promise(function)

//arrow function
let promise = new Promise( ( )=>{...} )

The function passed onto new Promise is called an executor. It contains the code that performs a task and produces the result(producing code). The executor takes some time to perform a task and get the result. That is, it performs an asynchronous task.

It also contains two arguments which are resolve and reject. These two arguments are provided by javascript and they are callbacks. If the executor performs the task successfully, then resolve is called. If the task being performed by the executor was not successful, reject is called.

resolve and reject are callbacks. So what executes them? The executor. In callbacks, the containing function executes the callback. So the executor runs first, and if there is a result, it executes resolve. If there is an error, It executes reject.

//Pseudocode
//Here, is an arrow function
let promise = new Promise( (resolve, reject)=>{
        //Code that performs a task and produces the result
        if(task was successful){
            resolve("successful")
        }else{
            reject("something went wrong")
        }
} )

Resolve and reject can only take one argument. resolve(value) or reject(error). value is the result after the successful completion of the task.

error is the error after the executor fails to complete the task. It is best to use an error object.

//with no arguments
resolve()
reject()

//with arguments
resolve(value) //Value is the result after the executor finished the task successfully
reject(error) //error is the error object

State and Result

The new Promise() constructor returns a promise object. The promise object has these properties:

1. State

A promise has three states: pending, fulfilled and rejected The executor moves the promise from one state to another.

The initial state is pending. This is when we have neither received a result nor an error. The state changes from pending to fulfilled when resolve is called. The state changes from pending to rejected when reject is called.

2. Result

The initial result is undefined. The result changes from undefined to value when resolve(value) is called. The result changes from undefined to error when reject(error) is called.

Example: Fetching data from a server using an API. The executor will perform the task of fetching the data. At this time while it is fetching, the state is pending and the result undefined. If it manages to get the data, it means the operation was successful. Resolve() is then called by the executor and produces the result/value and the state changes from pending to fulfilled. If it fails to get the data, then the operation wasn't successful hence reject is called and the state changes from pending to rejected and the result from undefined to error.

let promise = new Promise( (resolve, reject)=>{
        let dataFromServer
        if(dataFromServer){
            resolve("We got the data")
        }else{
            reject("Data not received")
        }
} )

You can also use a function that returns a promise :

function myPromise(){
return new Promise( (resolve, reject)=>{
let dataFromServer
if(dataFromServer){
    resolve("We got the data")
}else{
    reject("Data not received")
}
} )

NB The executor can only call one resolve or reject. If there are other calls to resolve or reject, they are ignored.

let promise = new Promise( (resolve, reject)=>{
          resolve(1) 
          reject(new Error("Oops something went wrong")) //ignored
          setTimeout(()=>{
              resolve(2)   //ignored
           }, 2000)
})

The executor can only produce one result or an error. It is also a good practice to use error object in reject.

We cannot directly access the state and result properties of the promise object because they are internal properties. We can however access them using the following methods: .then, .catch, .finally.

.then, .catch and .finally

After the producing code performs a task and gets the result, we usually want to do something with that result. But we cannot directly access that result. We have to use the .then method.

.then

.then method runs immediately after the promise has been fulfilled/ after resolve is called.

.then takes a callback function which receives the result when resolve is called. ie. .then(function). The callback function inside .then takes result as an argument. Result is the value that we passed on to resolve as an argument when calling it. ie. resolve(value)

So here is a structure of a .then method:

//function expression
promise.then(function(result){
          //code that does something with the result
})
//arrow function
promise.then( (result)=>{
          //code that does something with the result
})

Here is an example of a promise with .then method

//Promise
let promise = new Promise( (resolve, reject)=>{
        setTimeout( ()=>{
            resolve("I am the result")
        }, 2000 )
} )
//.then
promise.then( (result)=>{
         alert(result)}     //an alert saying "I am the result" will be shown
)
//nb: .then runs immediately after resolve is called. The value/argument inside resolve() is the result, which is then passed as an argument in the function inside .then method.

NB .then can take two parameters. One is a callback function that receives the result when resolve is executed, and another callback function that receives the error when reject is executed.

//The result comes from the argument given to resolve- resolve(value).
//The error comes from the argument given to reject- reject(error).
promise.then( (result)=>{//Do something with the result},  (error)=>{//Do something with the error})

However, there is a better way of handling errors using .catch. Therefore, we can give .then one callback function that receives the result.

//promise.then(function)
promise.then( (result)=>{
      //"Do something with the result"
} )

sometimes we have to join many .then methods instead of just one. We shall discuss this in my next article on promise chaining

.catch .catch runs immediately after reject(error) is executed (by the executor). That is after the promise has been rejected. promise.catch(errorHandlingFunction)

let promise = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                reject("I am an error")
            }, 2000)
})
//.catch
promise.catch((error)=>{
            alert(error) //an alert saying "I am an error" will appear
})

NB: .catch(errorHandlingFunction) == .then(null, errorHandlingFunction)

.finally

The finally handler runs after the promise is settled. Settled means that the promise is either fulfilled or rejected. Whether the promise was fulfilled or rejected, finally is always going to run. It does not process a promise but rather finalizes everything after the previous operations have been completed. The finally handler takes one parameter which is a function.

.finally( ()=>{
    //task to be performed when the promise is settled
} )

Final thoughts Promises are flexible. If the promise is still pending, .then, .catch and .finally wait for the outcome. If the promise is already settled when we add a handler, the handler runs immediately. Handlers can be added at any time and if the result is already there, they just execute.

EXAMPLE

let promise = new Promise( (resolve, reject)=>{
      setTimeout(()=>{
            let count = 5
      if(count == 5){
            resolve("done")
     }else{
            reject(new Error("Ooops something went wrong))
    }
    }, 2000)
} )
promise
.then((result)=>{
      console.log(result)
})
.catch((error)=>{
      console.log(error)
})
.finally(()=>{
      console.log("Everything is finalized")
})

solution: The conditional if statement evaluates to true that count == 5. resolve("done") is then called and the value "done" is passed onto .then as the result. Hence "done" is logged on to the console. .finally is then executed and logs "Everything is finalized" to the console. The console:

promise.PNG

Guess the outcome

let promise = new Promise( (resolve, reject)=>{
      setTimeout(()=>{
            let count = 4
      if(count == 5){
            resolve("done")
     }else{
            reject(new Error("Ooops something went wrong))
    }
    }, 2000)
} )
promise
.then((result)=>{
      console.log(result)
})
.catch((error)=>{
      console.log(error)
})
.finally(()=>{
      console.log("Everything is finalized")
})

solution:

error.PNG

.catch will handle the error and .finally will still run. .then will be ignored.

Check out my next article on promise chaining