DEV Community

John Au-Yeung
John Au-Yeung

Posted on • Edited on

Introduction to JavaScript Promises and Async Programming

Subscribe to my email list now at http://jauyeung.net/subscribe/

Follow me on Twitter at https://twitter.com/AuMayeung

Many more articles at https://medium.com/@hohanga

Because JavaScript is a single-threaded language, synchronous code can only be executed one line at a time. This means that if there’s synchronous code that runs longer than instantaneously, then it will stop the rest of the code from running until whatever is being run is finished. To prevent code that runs for an indeterminate amount of time from holding up other code from running, we need to have asynchronous code.


Promises

To do this in JavaScript, we can use promises in our code. Promises are objects that represent a process that runs for an indeterminate amount of time and may result in success or failure. To create promises in JavaScript, we use a Promise object that’s a constructor for creating promises. The Promise constructor takes a function called the executor function that has the resolve and reject parameters. Both parameters are functions that we call to either fulfill the promise (which means end it successfully with some value) or to reject the promise (which means returning some error value and marking the promise as failed). The function’s return value is ignored. Therefore, promises can’t return anything other than promises.

For example, we can define a promise in JavaScript like in the following code:

const promise = new Promise((resolve, reject) => {  
  setTimeout(() => resolve('abc'), 1000);  
});

The code above creates a promise that fulfills the promise with the value abc after one second. Because we’re running setTimeout inside the executor function to resolve the promise with the value abc in one second, it’s asynchronous code. We can’t return the value abc within the callback function of setTimeout, so we have to call resolve('abc') to get the resolved value back. We can access the resolved value of the fulfilled promise by using the then function. The then function takes a callback function that takes the resolved value of the fulfilled promise as a parameter. You can get the value there and do what you want with it. For example, we can do something like:

const promise = new Promise((resolve, reject) => {  
  setTimeout(() => resolve('abc'), 1000);  
});

promise.then((val) => {  
  console.log(val);  
})

When we run the code above, we should see abc logged. As we can see, the promise will supply the value when the promise is fulfilled with a call to the resolve function.

A promise has three states. It can be pending, which means that the promise has been fulfilled or rejected. It can be fulfilled, which means that the operation completed successfully. Or it can be rejected, which means that the promise operation failed.

A pending promise can either be fulfilled with a value or be rejected with some error. When a promise is fulfilled, then the corresponding resolve value is picked up by the then function and the callback function passed into the then function is called. If a promise is rejected, then we can optionally catch the error with the catch function, which also takes a callback function with the error. Both the then and catch functions return promises, so they can be chained together.

For example, we can write something like:

const promise = (num) => {  
  return new Promise((resolve, reject) => {  
    setTimeout(() => {  
      if (num === 1) {  
        resolve('resolved')  
      } else {  
        reject('rejected')  
      }  
    }, 1000);  
  });  
}

promise(1)  
  .then((val) => {  
    console.log(val);  
  })  
  .catch((error) => {  
    console.log(error);  
  })promise(2)  
  .then((val) => {  
    console.log(val);  
  })  
  .catch((error) => {  
    console.log(error);  
  })

In the code above, we have a function promise that returns a JavaScript promise that fulfills the promise with the value resolved when num is 1, and reject the promise with error rejected otherwise. So we run:

promise(1)  
  .then((val) => {  
    console.log(val);  
  })  
  .catch((error) => {  
    console.log(error);  
  })

Then the then function runs, the promise returned by the promise(1) function call is fulfilled since num is 1, and the resolved value is available set in val. So when we run console.log(val) we get resolved logged. When we run the code below:

promise(2)  
  .then((val) => {  
    console.log(val);  
  })  
  .catch((error) => {  
    console.log(error);  
  })

Then the catch function runs because the promise returned by the promise(2) function call is rejected, and the rejected error value is available set in error. So when we run console.log(error) we get rejected logged.

A JavaScript promise object has the following properties: length and prototype. The length always has its value set to 1 for the number of constructor arguments, which is always one. The prototype property represents the prototype of the promise object.

A promise also has the finally method to run code that’s run no matter whether the promise is fulfilled or rejected. The finally method takes a callback function as an argument, where any code that you want to run, regardless of the outcome of the promise, is run. For example, if we run:

Promise.reject('error')  
  .then((value) => {  
    console.log(value);  
  })  
  .catch((error) => {  
    console.log(error);  
  })  
  .finally(() => {  
    console.log('finally runs');  
  })

Then we get error and finally runs logged because the original promise was rejected with the reason error. Then whatever code in the callback of the finally method runs.

The main benefit of using promises for writing asynchronous code is that we can use them to run asynchronous code sequentially. To do this, we can chain promises with the then function. The then function takes a callback function that runs when the promise is fulfilled. It also takes a second argument when the promise is rejected. To chain promises, we have to return another promise as the return value of the first callback function of the then function. It can return other values, like nothing, if we don’t want to chain another promise to the existing promise. We can return a value, which will be resolved and the value retrievable in the next then function. It can also throw an error. Then the promise returned by then gets rejected with the thrown error as the value. It can also return an already fulfilled or rejected promise, which will get the fulfilled value when a then function is chained after it or get an error reason in the callback of the catch function.

For example, we can write the following:

Promise.resolve(1)  
  .then(val => {  
    console.log(val);  
    return Promise.resolve(2)  
  })  
  .then(val => {  
    console.log(val);  
  })Promise.resolve(1)  
  .then(val => {  
    console.log(val);  
    return Promise.reject('error')  
  })  
  .then(val => {  
    console.log(val);  
  })  
  .catch(error => console.log(error));Promise.resolve(1)  
  .then(val => {  
    console.log(val);  
    throw new Error('error');  
  })  
  .then(val => {  
    console.log(val);  
  })  
  .catch(error => console.log(error));

In the first example, we chained the promises and they all resolve to a value. All the promises were already resolved into values. In the second and last examples, we reject the second promise or throw an error. They both do the same thing. The second promise is rejected, and the error reason will be logged in the callback of the catch function. We can also chain pending promises, like in the following code:

const promise1 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(1), 1000);  
});

const promise2 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(2), 1000);  
});

promise1  
  .then(val => {  
    console.log(val);  
    return promise2;  
  })  
  .then(val => {  
    console.log(val);  
  })  
  .catch(error => console.log(error));

The callback of the first then function returned promise2, which is a pending promise.


Methods

JavaScript promises have the following methods.

Promise.all(iterable)

Promise.all takes an iterable object that lets us run multiple promises in parallel in some computers and serially in other computers. This is handy for running multiple promises that don’t depend on each other’s values. It takes in an iterable with a list of promises, usually an array, and then returns a single Promise that’s resolved when the promises that are in the iterable are resolved.

For example, we can write code like the following to run multiple promises with Promise.all:

const promise1 = Promise.resolve(1);  
const promise2 = 2;  
const promise3 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(3), 1000);  
});  
Promise.all([promise1, promise2, promise3])  
  .then((values) => {  
    console.log(values);  
  });

If we run the code above, then the console.log should log [1,2,3]. As we can see, it only returns a resolved value after all the promises are fulfilled. If some of them are rejected, then we won’t get any resolved value. Instead, we will get whatever error value is returned by the rejected promises. It will stop at the first rejected promise and send that value to the callback of the catch function. For example, if we have:

const promise1 = Promise.resolve(1);  
const promise2 = Promise.reject(2);  
const promise3 = new Promise((resolve, reject) => {  
  setTimeout(() => reject(3), 1000);  
});  
Promise.all([promise1, promise2, promise3])  
  .then((values) => {  
    console.log(values);  
  })  
  .catch(error => {  
    console.log(error);  
  });

Then we get 2 logged from the console.log in the callback of the catch function.

Promise.allSettled

Promise.allSettled returns a promise that resolves after all the given promises are resolved or rejected. It takes an iterable object with a collection of promises, for example, an array of promises. The resolved value of the returned promise is an array of the final status of each promise. For example, suppose we have:

const promise1 = Promise.resolve(1);  
const promise2 = Promise.reject(2);  
const promise3 = new Promise((resolve, reject) => {  
  setTimeout(() => reject(3), 1000);  
});  
Promise.allSettled([promise1, promise2, promise3])  
  .then((values) => {  
    console.log(values);  
  })

If we run the code above, then we get an array with three entries, with each entry being an object that has the status and value properties for the fulfilled promises and an object that has the status and reason properties for the rejected promises. For example, the code above will log {status: “fulfilled”, value: 1}, {status: “rejected”, reason: 2}, {status: “rejected”, reason: 3} . The fulfilled status is logged for the successful promises and therejected status for the rejected ones.

Promise.race

The Promise.race method returns a promise that resolves to the resolved value of the promise that’s fulfilled first. It takes an iterable object with a collection of promises, for example, an array of promises. If the iterable passed in is empty, then the returned promise will forever be pending. If the iterable object contains one or more non-promise values or already settled promises, then Promise.race will return to the first of these entries. For example, if we have:

const promise1 = Promise.resolve(1);  
const promise2 = Promise.resolve(2);  
const promise3 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(3), 1000);  
});  
Promise.race([promise1, promise2, promise3])  
  .then((values) => {  
    console.log(values);  
  })

Then we see that 1 is logged. That’s because promise1 is the first one that’s resolved since it’s resolved instantaneously before the next lines are run. Likewise, if we have a non-promise value in the array that we pass in as the argument, like in the following code:

const promise1 = 1;  
const promise2 = Promise.resolve(2);  
const promise3 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(3), 1000);  
});  
Promise.race([promise1, promise2, promise3])  
  .then((values) => {  
    console.log(values);  
  })

Then we get the same thing logged since it’s the non-promise value in the array that we pass into the Promise.race method. Synchronous code always runs before the asynchronous code, so it doesn’t matter where the synchronous code is. If we have:

const promise1 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(1), 2000);  
});

const promise2 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(2), 1000);  
});

const promise3 = 3;  
Promise.race([promise1, promise2, promise3])  
  .then((values) => {  
    console.log(values);  
  })

Then we get 3 logged since setTimeout puts the callback function in the queue to be run later, so it’s going to be run later than the synchronous code.

Finally, if we have:

const promise1 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(1), 2000);  
}); 

const promise2 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(2), 1000);  
});

Promise.race([promise1, promise2])  
  .then((values) => {  
    console.log(values);  
  })

Then we get 2 in the console log because a promise that’s resolved in one second is going to be resolved earlier than a promise resolved in two seconds.

Promise.reject

Promise.reject returns a promise that’s rejected with a reason. It’s useful to reject a promise with an object that’s an instance of Error. For example, if we have the following code:

Promise.reject(new Error('rejected'))  
  .then((value) => {  
    console.log(value);  
  })  
  .catch((error) => {  
    console.log(error);  
  })

Then we get rejected logged.

Promise.resolve

Promise.resolve returns a promise that’s resolved to the value that’s passed into the argument of the resolve function. We can also pass in an object with the then property where the value of it is the callback function of a promise. If the value has a then method, then the promise will be fulfilled with the value fulfilled by the then function. That is, the first parameter of the function for the value of the then function is the same as resolve, and the second parameter is the same as reject. For example, we can write the following:

Promise.resolve(1)  
  .then((value) => {  
    console.log(value);  
  })

Then we get 1 logged since 1 is the value we passed into the resolve function to return the promise with resolved value 1.

If we pass in an object with a then method inside, like in the following code:

Promise.resolve({  
    then(resolve, reject) {  
      resolve(1);  
    }  
  })  
  .then((value) => {  
    console.log(value);  
  })

Then we get value 1 logged. That’s because the Promise.resolve function will run the then function, and the resolve parameter for the function set to the then property will be assumed to be a function that’s called the resolve function in a promise. If we instead called the reject function inside the then function in the passed-in object, then we get a rejected promise, like in the following code:

Promise.resolve({  
    then(resolve, reject) {  
      reject('error');  
    }  
  })  
  .then((value) => {  
    console.log(value);  
  })  
  .catch((error) => {  
    console.log(error);  
  })

In the code above, we get error logged since the promise is rejected.


Async and Await

With async and await, we can shorten the promise code. Before async and await, we had to use the then function and we had to put callback functions as an argument of all of our then functions. This makes the code long as we have lots of promises. Instead, we can use the async and await syntax to replace the then function and its associated callbacks. For example, we can shorten the following code from:

const promise1 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(1), 2000);  
});

const promise2 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(2), 1000);  
});

promise1  
  .then((val1) => {  
    console.log(val1);  
    return promise2;  
  })  
  .then((val2) => {  
    console.log(val2);  
  })

to:

const promise1 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(1), 2000);  
});  

const promise2 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(2), 1000);  
});

(async () => {  
  const val1 = await promise1;  
  console.log(val1)  
  const val2 = await promise2;  
  console.log(val2)  
})()

We replaced the then and callbacks with await. Then we can assign the resolved values of each promise as variables. Note that if we use await for our promise code, then we have to put async in the function signature as we did in the above example. To catch errors, instead of chaining the catch function at the end, we use the catch clause. Also, instead of chaining the finally function at the bottom to run code when a promise ends, we use the finally clause after the catch clause.

For example, we can write:

const promise1 = new Promise((resolve, reject) => {  
  setTimeout(() => resolve(1), 2000);  
});  
const promise2 = new Promise((resolve, reject) => {  
  setTimeout(() => reject('error'), 1000);  
});

(async () => {  
  try {  
    const val1 = await promise1;  
    console.log(val1)  
    const val2 = await promise2;  
    console.log(val2)  
  } catch (error) {  
    console.log(error)  
  } finally {  
    console.log('finally runs');  
  }})()

In the code above, we got the resolved value of the promise assigned to a variable instead of getting the value in the callback of the then function, like in the const response = await promise1 line above. Also, we used the try...catch...finally blocks to catch the errors of rejected promises and the finally clause instead of the finally function with a callback passed in to create code that runs no matter what happens to the promises.

Like any other function that uses promises, async functions always return promises and cannot return anything else. In the example above, we showed that we can chain promises in a much shorter way than with the then function with callbacks passed in as an argument.


Conclusion

With promises, we can write asynchronous code with ease. Promises are objects that represent a process that runs for an indeterminate amount of time and may result in success or failure. To create promises in JavaScript, we use a Promise object that’s a constructor for creating promises.

The Promise constructor takes a function called the executor function that has the resolve and reject parameters. Both parameters are functions that we call to either fulfill the promise (which means end it successfully with some value) or to reject the promise (which means returning some error value and marking the promise as failed). The function’s return value is ignored. Therefore, promises can’t return anything other than promises.

Promises are chainable since promises return promises. The then function of promises can throw an error, return resolved values, or return other promises that are pending, fulfilled, or rejected.

Top comments (4)

Collapse
 
alexandis profile image
alexandis

Great article! Explains promises easily and exhaustively.

Collapse
 
aumayeung profile image
John Au-Yeung

Thanks so much for reading.

Collapse
 
_fraganya profile image
Francis Ganya

Thank you , very exhaustive

Collapse
 
aumayeung profile image
John Au-Yeung

Thanks so much for reading.