Breaking Down JavaScript Promises

Photo by Womanizer Toys on Unsplash

JavaScript is a single-threaded language, meaning the code is run line by line. When the first line has finished executing, the second one can start.

Unfortunately, if a single line takes a long time to execute, such as waiting for a response to a web service call, the whole program will take time to run.

One possible way to remedy this situation is by using asynchronous programming. An asynchronous piece of code executes in the background without blocking the main thread. (

There are two ways to handle asynchronous operations in JS:

  1. Observables
  2. Promises 

Promises

A promise is native to JavaScript. We do not require any other libraries to be able to get promises up and running in our code.

A promise promises some data. It will undoubtedly return data even if no code is using it.

The server collects all of the data and, once complete, sends it all to the client at once.

Structure of a promise

The code for a promise can be broken into 2 parts:

  1. The producing code. This is the code that can take time
  2. The consuming code. This is the code that must wait for the result.

A promise is a JavaScript Object that links the producing and consuming codes.

Below is a simple promise

// This part is called the executor
const myPromise = new Promise((resolve, reject) => {
  const randomNum = Math.floor(Math.random() * 10);
  if (randomNum % 2 === 0) {
    resolve(randomNum);
  } else {
    reject('Error: Random number is odd');
  }
});

myPromise
  .then((result) => console.log(result))
  .catch((error) => console.log(error))
  .finally(() => console.log('done'));

"new Promise" on line 2 is called the executor or the producing code. When it is created, the executor runs automatically.

It contains the producing code that will eventually produce the result.

We create a new Promise using the "new Promise()" constructor. The promise takes a function as an argument, which has two parameters: resolve and reject.

When the executor receives the result, be it sooner or later, it should call one of the callbacks listed below: 

  1. resolve(value) - if the job is finished successfully with value 'value'.

       2. reject(error) - if an error has occurred, 'error' is the error object.

As from line 11, we have the consuming code.

On line 12, we see the function 'then()'. This function runs when the promise is resolved and received the result. It is used to handle the result of the promise.

The 'then()' method takes a function as an argument, which is called when the promise is resolved successfully.

On line 13, we see the 'catch()' function. It is called when an error/failure occurs.

On line 14. there is a special function, 'finally()'. It always runs when the promise is settled, be it resolved or rejected. Note that it has no argument.

'finally()' is used to setup a handler for performing cleanup/finalizing after the previous operations are completed; for example, stopping loading indicators or closing no longer needed connections.

These codes and functions can be found in the preceding code.

The executor is the first part of the code. A random number is generated in this section, and the 'if condition' determines whether the number is even or oddThe promise calls the 'resolve()' function if the number is even. Else, the 'reject()' function is called with an error message.

The consuming code is the second part of the code. We can see the 'then()', 'catch()' and 'finally()' methods in action there. When the resolve() function from the executor code is called (meaning that a random number is an even number), the value is passed to the then() method and is logged in the console.

The 'catch()' method is called when the random number generated is an odd number, and the reject() is called in the executor code. 

The 'finally()' method is called when the promise is fulfilled. It is unaffected by whether resolve() or reject() is called. Once the message is logged, either by the 'then()' or 'catch()' methods, the finally() method logs the message 'done' in the console.

Below is the result in the console when an even number is generated.

Below is the result in the console when an odd number is generated.

Using promises for a web service call

Web service calls are a good example of blocking codes. When a web service call is done, the server does some processing and returns the requested information. This process can take several seconds.

To prevent any impact or delay on our program, the web service calls are done in a promise.

const myPromise = new Promise((resolve, reject) => {
  fetch('https://jsonplaceholder.typicode.com/posts/1')
  .then((response) => response.json())
  .then((data) => {
    resolve(data)
  })
  .catch((error) => reject(error))
});

console.log('this is the start');

myPromise
.then((result) => console.log(result))
.catch((error) => console.log(error))
.finally(() => console.log('promise done'));

console.log('this is the end');

In the executor, we use JavaScript's fetch API to get post details about the first post from the API 'https://jsonplaceholder.typicode.com/posts/1'. Being potentially a time consuming operation, it is a completely valid use of promises. The 'fetch(URL)' method in 'new Promise' also creates a promise that makes the HTTP request to the specified URL and resolves the response object when the response is received. On line 3, the 'then()' method is a method chain on the promise created by the 'fetch(URL)which returns another promise that resolves the parsed JSON data. It is possible because the '.json()' method reads the response body in JSON format asynchronously. The 'then()' on line 4 is another method chain on that promise that takes the parsed JSON returned by the first 'then()' as an argument and emits it. The result is emitted by the resolve() method on line 5. If this operation fails, an error will be returned by the catch() function on line 7.

Note:  The fetch API is a different topic on its own. The above explanation is applicable only to the Promise use case. To know more about it, please check the documentation of fetch API. When using promises in your code, the structure provided above can stay the same. You just need to change the URL of your web service, and your code will work fine, provided that the web service you are calling returns a json.

The consuming code, starting from line 12, is similar to the first example. Note that, at the start of the consuming code, on line 10, and after its end, on line 17, we have added two console logs. These logs will help see the asynchronous nature of this code in action.

Below is the result of the above code:

 

The console log indicating the start is written at the start as expected, but the one indicating the end is outputted immediately after the one indicating the start as if the consuming code is not present. It is because a web service call is made, and we are making use of a promise for this purpose. As such, this part is running asynchronously leaving the main thread free to continue executing the rest of the code; in this case, writing the console log indicating the end.

When the promise is resolved, the result is displayed, and the 'finally()' is called, writing 'promise done' as indicated in the code.

Chaining promises

There can be cases where the result of the first API call is passed as a parameter to the second API call. This operation is referred to as chaining. Since we are using promises to execute API calls, it is called chaining promises.

const myPromise = new Promise((resolve, reject) => {
  fetch('https://jsonplaceholder.typicode.com/posts/1')
    .then((response) => response.json())
    .then((post) => {
      const userId = post.userId;
      return fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
    })
    .then((response) => response.json())
    .then((data) => {
      resolve(data);
    })
    .catch((error) => reject(error));
});

console.log ('this is the start');

myPromise
  .then((result) => console.log(result))
  .catch((error) => console.log(error))
  .finally(() => console.log('done'));

  console.log ('this is the end');

The structure of the executor code is almost the same. On line 4, we can see the second 'then()'. Instead of resolving the promise as in the previous example, we are extracting the userId from the response of the first API call. Note that the result of the first API call is called 'post'.

'post.userID' on line 5 extracts the value of the userID from the response. The userId is saved in a constant and is passed as a parameter in the second call done on line 6. The resolve() on line 10 will emit the result/data of the second API call when it is successful. In case of failure during this operation, an error will be return by the catch() on line 12.

The consuming code remains the same as for the previous example.

As in the previous example, we can also observe the asynchronous nature of this code.

Limitation of promises

  1. Lack of cancellation: Once a promise is created, there is no built-in way to cancel it. Once a promise is initiated, it will run to completion, either by resolving with a value or by rejecting with an error.

  2. Complex error handling: While promises simplify asynchronous programming, they can also make error handling more complex, especially when dealing with complex, nested chains of Promises

  3. No support for synchronous errors: Promises can only handle asynchronous errors, not synchronous errors that occur during the execution of the code. If a synchronous error occurs, it will be thrown as an exception and needs to be caught using a try-catch block.

  4. Not supported in all browsers: Promises were introduced in ES6 and are not supported in older browsers. Although polyfills are available to support Promises in older browsers, it's still a limitation that needs to be considered when building applications that rely on Promises.

  5. Callback functions: Although promises provide a way to avoid "callback hell", it can still be difficult to use promises in situations where you need to work with APIs that only provide callback-based APIs, like some of the Node.js APIs.

Conclusion

The promise is a powerful way of writing asynchronous code in JavaScript. It does, however, have some limitations that must not be overlooked. It is important to correctly understand what is expected from the code to determine whether a promise is the best option. If using a promise is inconvenient, consider using Observables.