blog post

Understanding Javascript Promises a Guide to Asynchronous Programming

Introduction

Asynchronous programming is a must-have in modern JavaScript development, allowing developers to perform non-blocking operations, such as fetching data from a server, reading files, or executing time-consuming operations. JavaScript promises are a powerful feature introduced in ES6 (ECMAScript 2015) to manage asynchronous operations more efficiently and cleanly. This blog post is a guide to JavaScript promises, offering a clear understanding how promises can be used to handle asynchronous tasks in an elegant way.

What Are JavaScript Promises?

A Promise in JavaScript is an object that represents the eventual completion or failure of an asynchronous operation and its resulting value. A Promise is a proxy for a value that is not necessarily known when the promise is created, this value can be evaluated at some point in the future.

Promises can be in one of three states:

  • Pending: The initial state of a promise. The operation has not completed yet.
  • Fulfilled: The operation completed successfully, and the promise has a resolved value.
  • Rejected: The operation failed, and the promise has a reason for the failure.

Creating and Using Promises

To create a promise, you can use the Promise constructor, which has one argument - a function called the executor. The executor function is executed immediately by the Promise implementation and it has two function parameters: resolve and reject callbacks.

javascript
const promise = new Promise((resolve, reject) => { try { // Execute operation resolve("Operation succeeded"); } catch (e) { reject("Operation failed"); } });

When a promise starts executing it's in the Pending state by default. Later promise must be transferred to either Fulfilled or Rejected state.

resolve callback is used to transfer a promise to Fulfilled state and return a resulting value of the promise as parameter. reject callback is used to transfer a promise to Rejected state and return a failure result of the promise as parameter.

There are 2 ways to get the promise result:

  • using then() and catch() functions that hook to the promise
  • using async/await statements

Get Promise Result Using then() and catch() Functions

To get a result from a promise, you can call then() function for success scenarios and catch() function for handling failures. For example:

javascript
promise .then(result => { // "Operation succeeded" if successful console.log(result); }) .catch(error => { // "Operation failed" if an error occurred console.error(error); });

You can chain these methods to perform additional operations after the previous promise has completed.

javascript
function firstPromise() { return new Promise((resolve, reject) => { setTimeout(() => { console.log("First promise finished with result: 1"); resolve(1); }, 1000); }); } function secondPromise(previousResult) { return new Promise((resolve, reject) => { setTimeout(() => { console.log("Second promise finished with result: ", previousResult + 1); resolve(previousResult + 1); }, 1000); }); } function thirdPromise(previousResult) { return new Promise((resolve, reject) => { setTimeout(() => { console.log("Third promise finished with result: ", previousResult + 1); resolve(previousResult + 1); }, 1000); }); } firstPromise() .then(result => secondPromise(result)) .then(result => thirdPromise(result)) .then(finalResult => console.log("All promises completed with final result: ", finalResult)) .catch(error => console.error("An error occurred:", error));

Here we chain together 3 promises executing one after another. In this example "3" is printed to console as a final result. The final catch() function is called when an error occurs or reject() function is called in any of 3 promises.

Before the invention of Promises performing asynchronous operations anticipated using callbacks. This led to a bad code structure with deeply nested callbacks, especially when using multiple asynchronous operations one after another.

Here is how the previous example will look like when using callbacks:

javascript
function firstCallback(callback) { setTimeout(() => { console.log("First callback finished with result: 1"); callback(1); }, 1000); } function secondCallback(previousResult, callback) { setTimeout(() => { console.log("Second callback finished with result: ", previousResult + 1); callback(previousResult + 1); }, 1000); } function thirdCallback(previousResult, callback) { setTimeout(() => { console.log("Third callback finished with result: ", previousResult + 1); callback(previousResult + 1); }, 1000); } firstCallback(function(result) { secondCallback(result, function(newResult) { thirdCallback(newResult, function(finalResult) { console.log("All callbacks completed with final result: ", finalResult); }); }); });

Doesn't look pretty, right? That's why the invention of promises was a huge step further in asynchronous JavaScript development. But what if I told you - that it's not the best viable option nowadays? Let's explore what is the best option.

Get Promise Result Using async/await

async/await statements in JavaScript are a complete game changer in asynchronous development. ES2017 introduced async functions and the await keyword, that allow developers to write asynchronous code that looks and behaves a like synchronous code:

javascript
async function fetchDataAsync() { try { const response = await fetch("https://jsonplaceholder.typicode.com/posts"); const data = await response.json(); return data; } catch (error) { console.error("Failed to fetch data: ", error); } } const data = await fetchDataAsync(); console.log(data);

Here an async function returns a Promise<T>, that holds a data received from the API call. By using await keyword we get this data as a promise result.

After the line const data = await fetchDataAsync(); we can simply write more code as if all operations were executed synchronously. It looks perfect!

Now let's explore how we can update one of the previous examples where we chained 3 promises with a then() function, when using async/await statements:

javascript
function firstPromise() { return new Promise((resolve, reject) => { setTimeout(() => { resolve(1); }, 1000); }); } function secondPromise(previousResult) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(previousResult + 1); }, 1000); }); } function thirdPromise(previousResult) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(previousResult + 1); }, 1000); }); } try { const firstResult = await firstPromise(); console.log("First promise finished with result: ", firstResult); const secondResult = await secondPromise(firstResult); console.log("Second promise finished with result: ", secondResult); const thirdResult = await thirdPromise(secondResult); console.log("Third promise finished with result: ", thirdResult); console.log("All promises completed with final result: ", thirdResult); } catch (error) { console.error("An error occurred:", error); }

It looks really nice. This code is much more readable and maintainable because of using the async/await statement.

Summary

JavaScript promises are a vital feature for performing non-blocking operations, such as fetching data from a server, reading files, or executing time-consuming operations. It offers a more manageable and readable approach compared to old callback-based solutions.

You can use the async/await syntax to write even cleaner and more efficient JavaScript code.

Hope you find this blog post useful. Happy coding!

Improve Your .NET and Architecture Skills

Join my community of 1800+ developers and architects.

Each week you will get 2 practical tips with best practises and architecture advice.