Understanding Promises in JavaScript

Understanding Promises in JavaScript

JavaScript Promises can be a tricky topic to wrap your head around, especially if you're just starting out with asynchronous programming. Even experienced developers can sometimes struggle with understanding how Promises work and when to use them. With so many online articles about Promises, it can be overwhelming to find a clear and concise explanation.

In this blog post, I'll be breaking down the concept of Promises into the most relevant sub-topics to help make it easier for anyone learning about this critical topic.

Prerequisites

There are only two things I believe are essential to know to follow along in this article. I have listed them below and added some explanations and references.

  • Asynchronous Programming: According to MDN, Asynchronous Programming is a technique that enables your program to start a potentially long-running task and still be responsive to other events while that async task keeps running, rather than having to wait until that task has finished.

  • Synchronous Programming: Exactly as it sounds, the code is executed on a line-by-line basis. Each line is run before the next line of code.

What is a Promise?

MDN defines a promise as a proxy for a value not necessarily known when the promise is created. It allows async operations to return a value even before the async operation is completed, such that you can attach functions that will run depending on if the promise got fulfilled or rejected. A Promise is a JavaScript Object, it can be in either of the following three states;

  1. Pending: This is the initial state. At this point, the async operation is still running.

  2. Fulfilled: In this state, the async operation ran and was completed successfully.

  3. Rejected: In this state, the async operation failed and is being rejected.

Creating a Promise

Most of the time, you’ll be handling the rejection/fulfillment of a promise rather than creating it, but pay attention as there are instances where you may have to create a promise too. Creating a promise entails declaring an instance of the Promise class in the manner below;

const promise = new Promise();

It’s important to note that the Promise constructor function accepts a function as an argument. This function has two parameters;

  1. the resolve method which is called to change the status of the promise to fulfilled

  2. the reject method which is called to change the status of the promise to rejected.

Such that;

const promise = new Promise((resolve, reject) => {
});

How To reject/fulfill a promise

To reject a promise, you’ll need to call the reject function, which takes an argument that will be the parameter of the callback function that will handle the promise rejection. In the same vain, the resolve function is called to resolve a promise passing it a value that will be the parameter in the callback function that handles a resolved promise.

const promise = new Promise((resolve, reject) => {
  // performs asynchronous operation

  // if promise fails - to set the status of the promise to rejected
  reject("This promise failed!");

  //if promise succeeds - to set the status of the promise to fulfilled
  resolve("Promise has been fulfilled");
});

Calling the reject method sets the status of the Promise to rejected, and calling the resolve method sets the status of the promise to fulfilled. You would typically call either of these functions after executing an asynchronous operation, depending on whether it failed or succeeded.

Handling a promise rejection or fulfillment

The promise instance gives access to two methods;

  • The then method takes a callback function that handles the Promise fulfillment. The then method also tasks an optional second argument called the rejectionCallbackFunction, which can be used instead of a catch method to handle a rejected promise. However, it is wildly recommended to use the catch method instead because it handles all errors that may occur and not just the Promise rejection.

  • The catch method takes a callback function that handles the Promise rejection and any other error that may have occurred in the fulfillmentCallbackFunction.

Something important to note here is that the callback functions have whatever value is passed as an argument to either the reject or resolve function, depending on the case, injected as a parameter. Such that we have;

// In case of a fulfilled Promise
promise.then(fulfillmentCallbackFunction) 

// In the case of a rejected Promise
promise.catch(rejectionCallbackFunction)

Where;

const fulfillmentCallbackFunction =(relsoveMethodArgument)=>{
  console.log(relsoveMethodArgument);
  // logs "Promise has been fulfilled" to the console.
}

And;

const rejectionCallbackFunction =(rejectMethodArgument)=>{
  console.log(rejectMethodArgument);
  // logs "This promise failed!" to the console.
}

How the JS runtime handles promises

It would be incomplete to discuss promises in JavaScript without discussing how the JavaScript engine handles them. Because of its single-threaded nature, the JS runtime uses the event loop mechanism to handle a promise. What is that? you may ask. The event loop is a constantly running process that monitors both the callback queue/Micro task queue and the call stack, checking if the call stack is empty to push pending queued functions to the call stack for execution. By this, the JavaScript engine is able to go on executing other lines of code while the promise is pending.

Summary

  • A promise is a proxy for a value not necessarily known when the promise is created.

  • You can create a Promise by declaring an instance of the Promise class and passing a callback function that has two parameters; resolve and reject.

  • Handling promise rejection or fulfillment can be done by passing respective callback functions to either the then or catch method depending on the status.

  • The JavaScript runtime handles promises with the event loop mechanism.

References