JavaScript Promise: designed for outstanding asynchronous operations

JavaScript Promise rolled out in ES6 in 2015, and it operates within a similar domain as a promise between people. As a person makes a promise, so does JavaScript, hence the name – Promise in JavaScript. Here is an example that includes both types of promises: "I promise to learn JavaScript Promise by the end of this year".

Human promises are not the result of spontaneous actions. When we make promises, we aim to keep them, although sometimes we fail. If I learn JavaScript Promise this year, then I will keep my word. In all other cases – I failed.
We conclude that you can either fulfill a promise or not. These binary conditions translate in JavaScript as two unique statuses: resolved or rejected. A fulfilled promise in JavaScript is a resolved one, while a failed one is a rejected promise. In asynchronous JavaScript-based operations, callbacks can create a mind-bending web of chaos. Since some tasks are waiting on other tasks to complete and have a somewhat undefined state or outcome, this is where JavaScript Promise comes to the rescue.

Callbacks and Asynchronous actions

Before we delve fully into Promise, let us look at how callbacks relate to asynchronous programming and why we need to know about both.
Programmers write a callback function as an argument; it later passes to another function and invokes there. The outer function can immediately pick the passing code and execute it. JavaScript supports callbacks and implements them in subroutines, lambda expressions, blocks, or function pointers.

There are two types of callbacks named based on how they control data flow at runtime. They are 1) synchronous callbacks which are the default callbacks, and 2) deferred callbacks, also called asynchronous callbacks.
Synchronous callbacks are executed immediately while asynchronous callbacks have delay associated with them.
Asynchronous in JavaScript refers to two events or threads happening simultaneously. In asynchronous programming, a program can make a request from another software or server and do other things in the background while it waits for a reply.

JavaScript Promise: improved code readability and error management

JavaScript Promise supports an asynchronous process that produces an unknown value at a later time. According to MDN "A Promise is a proxy for a value not necessarily known when the promise is created. It allows you to associate handlers with an asynchronous action's eventual success value or failure reason."

JavaScript is single-threaded, meaning two or more scripts cannot run at the same time. Scripts must always run after each other. This condition makes programming sluggish, time-consuming, and monotonous. For example, when a single-thread type network sends a request, it idles until a response. Besides, it can only handle one request at a time. A vast room for optimizations lies here.
Objects and events in programming try to emulate human behavior, and JavaScript's single threading is far from achieving this goal. Humans operate in a multi-threaded manner. We perform different actions and process different thoughts simultaneously and asynchronously. Most humans can lead meaningful conversations while cooking, for example.

Promises, callbacks, and asynchronous programming are great for multi-thread operations. The main advantage of Promise in JavaScript is that it allows multiple requests and time-consuming actions to perform simultaneously without blocking the main thread.

Schematic Explanation how Promises work

The Syntax of JavaScript Promise

You can create a promise object by using the ‘new’ keyword followed by ‘promise’. It takes a function and one argument, with two default callbacks - reject and resolve.

var dosomething = new Promise(function(resolve, reject) {
  // code goes here
  if (/* expression goes here */) {
    resolve("Yes, this works!");
  }
  else {
    reject("Oh No! It didn't work");
  }
});

Resolved holds the output for a successful promise, while reject – that for an unsuccessful promise.

.then() and .catch() handlers

.then() and .catch() are two handlers attached to callbacks in promises. .then() is attached to a resolved promise while .catch() is attached to a rejected promise.
Let us look at a demo showing how to use promises and their handlers.

let man = new Promise(function(resolve, reject){
  man_full = true;
  if(man_full)
    resolve()
  else
    reject()
});

man.then(function(){
  document.write("Stop Eating!")
}).catch(function(){
  document.write("You stop eat more!")
})

//output: Stop Eating
//if we change the value of man_full to false, it becomes 'You should eat more'

Promises work with arrow functions, now part of ES6. In the following demo, let us use an arrow function with parameters to give custom outputs.

let sum = new Promise((resolve, reject) =>{
  let add = 24 + 24;
  if (add == 49){
    resolve('Correct Answer!')
  }
  else {
    reject('Incorrect!')
  };
});

sum.then((custom_msg) =>{
    document.write("This is a "+ custom_msg)
}).catch((custom_msg) => {
    document.write("This is " + custom_msg)
});

//the custom msg with resolve callback is appended to the message for .then()
//the custom msg with reject callback is appended to the message for .catch()

Static methods of JavaScript Promise

1. Promise.all()

Let us review a promise API that executes two or more promises in parallel. In the following JavaScript promise example, we have to download specific files all at once. Once they are resolved, we can process them and assign them values.
It takes an array of promises and returns a new promise. The new promise is the value:

const download_one = new Promise((resolve, reject) =>{
  setTimeout(() => {
    resolve('First download completed');
  }, 5000);
})

const download_two = new Promise((resolve, reject) =>{
  setTimeout(() => {
    resolve('Second download completed');
  }, 3000);
})
const download_three = new Promise((resolve, reject) =>{
  setTimeout(() => {
    resolve('Third download completed');
  }, 1000);
})

Promise.all([
  download_one,
  download_two,
  download_three
]).then((msgs) => {
  document.write(msgs);
}).catch((msgs) => {
  document.write(msgs);
})

The output takes about five seconds to come on display since it takes that much time for the first download.

This JavaScript Promise example shows just how promise.all() works, executing and processing actions in parallel. If for any reason any of the promises listed is failed or rejected, promise.all() rejects everything, and the error message comes in display.

const download_one = new Promise((resolve, reject) =>{
  setTimeout(() => {
    resolve('First download completed');
  }, 5000);
})

const download_two = new Promise((resolve, reject) =>{
  setTimeout(() => {
    reject(new Error('Error with second download'));
  }, 3000);
})

const download_three = new Promise((resolve, reject) =>{
  setTimeout(() => {
    resolve('Third download completed');
  }, 1000);
})

Promise.all([
  download_one,
  download_two,
  download_three
]).then((msgs) => {
  document.write(msgs);
}).catch((msgs) => {
  document.write(msgs);
})

2. Promise.allSettled()

Since promise.all() takes an 'all or nothing' approach where it rejects the entire list, if one promise is rejected, promise.allSettled() takes a more relaxed approach. Instead, it shows the status of each promise according to the value or result.

Fulfilled promise = result

Rejected promise = error

const download_one = new Promise((resolve, reject) =>{
  setTimeout(() => {
    resolve('First download completed');
  }, 5000);
})

const download_two = new Promise((resolve, reject) =>{
  setTimeout(() => {
    reject(new Error('Error with second download'));
  }, 3000);
})

const download_three = new Promise((resolve, reject) =>{
  setTimeout(() => {
    resolve('Third download completed');
  }, 1000);
})

Promise.allSettled([
  download_one,
  download_two,
  download_three
]).then((msgs) => {
  console.log(msgs);
}).catch((msgs) => {
  console.log(msgs);
})

/*
Output
0:{status: "fulfilled", value: "First download completed"}
1: {status: "rejected", reason: Error: Error with second download}
2: {status: "fulfilled", value: "Third download completed"}
*/

3. Promise.race()

promise.race() returns an output once one of the promises completes. Unlike promise.all(), it does not wait for the whole list of promises to complete before it returns a value. It does not matter if that promise is a fulfilled or rejected one.

const download_one = new Promise((resolve, reject) =>{
  setTimeout(() => {
    resolve('First download completed');
  }, 5000);
})

const download_two = new Promise((resolve, reject) =>{
  setTimeout(() => {
    reject(new Error('Error with second download'));
  }, 3000);
})

const download_three = new Promise((resolve, reject) =>{
  setTimeout(() => {
    resolve('Third download completed');
  }, 1000);
})

Promise.race([
  download_two,
  download_one,
  download_three
]).then((msgs) => {
  console.log(msgs);
}).catch((msgs) => {
  console.log(msgs);
})

//OUTPUT
//Third download completed

The output says 'Third download completed', because the value for the third download is displayed first since it has the shortest timeout.

4. Promise.any()

Promise.any() behaves similarly to promise.race(), but it picks the first fulfilled or resolved promise to display.

const download_one = new Promise((resolve, reject) =>{
  setTimeout(() => {
    resolve('First download completed');
  }, 5000);
})

const download_two = new Promise((resolve, reject) =>{
  setTimeout(() => {
    reject(new Error('Error with second download'));
  }, 1000);
})

const download_three = new Promise((resolve, reject) =>{
  setTimeout(() => {
    resolve('Third download completed');
  }, 3000);
})

Promise.any([
  download_two,
  download_one,
  download_three
]).then((msgs) => {
  console.log(msgs);
}).catch((msgs) => {
  console.log(msgs);
})

//OUTPUT
//Third download completed

The second download is reset to have the shortest timeout of one second. But because it is a failed promise, it was not acknowledged, and, instead, the second shortest download is selected.

States of a Promise

A promise has four states or behavior

  • Pending:
    The pending state of a promise is the waiting state of the promise. In this state we neither have a resolved nor rejected the promise.
  • Fulfilled:
    An asynchronous operation that completes successfully; it returned a value or a fulfilled promise.
  • Rejected:
    An asynchronous operation was not successful; it returned an error and a rejected promise.
  • Settled:
    A processed promise that has been rejected or fulfilled. It is the final outcome of an asynchronous promise operation. A settled promise is immutable.

Main Features of JavaScript Promise

  1. A promise has a defined asynchronous nature
  2. It has two default callbacks - reject and resolve
  3. The output, value, or result is unknown at the time of creation
  4. A great solution to issues that arise from simple and nested callbacks
  5. The result of a promise is immutable
  6. its state does not change

Conclusion

JavaScript Promise is a modern approach to match the schemes and requirements of modern browsers and web applications. However, if a browser compatibility issue arises, it could be overridden thanks to polyfills.

Using promise with asynchronous operations has many benefits. It flattens the code, makes it more readable and maintainable. It also helps to handle asynchronous operations better, much better than callbacks.

For more tips and tricks about how to become better in programing, please check our software development blogs.

Author

Ivaylo Ivanov

Ivaylo loves Frontend implementations. He is eager to construct interfaces that users love; pixel-perfect. In his day-to-day job, he caters to anything HTML, CSS, SCSS, or JavaScript-based on the frontend side. Complexity is not a problem, he masters VueStorefront implementations equally well to simple web apps or PWAs.
Experimentation with CSS and its' capabilities is Ivo's passion. Be it animated elements, or SVG animations - he loves it!