1. javascript
  2. /async
  3. /promises

Promises in Asynchronous JavaScript

Introduction

Promises are a powerful feature introduced in ES6 that allows developers to handle asynchronous code elegantly and improve readability. They provide a way to handle the results of asynchronous operations, such as network requests, and handling multiple callback functions.

Basic Syntax

Promises are created using the Promise constructor. The constructor takes a single argument, a function called the executor, which is invoked immediately when the promise is created. The executor function takes two arguments, a resolve function, and a reject function, which are used to signal the completion or failure of the asynchronous operation.

const promise = new Promise((resolve, reject) => {
  // Asynchronous operation
  if (success) {
    resolve(result);
  } else {
    reject(error);
  }
});

Once a promise is created, it can be in one of three states:

  • pending: The initial state, the promise is neither fulfilled nor rejected.

  • fulfilled: The promise has been completed successfully and the result is available.

  • rejected: The promise has failed and an error is available.

The Promise Object

Promise objects have several methods that can be used to attach callbacks and handle the results of the asynchronous operation. The most commonly used methods are .then() and .catch().

The .then() method is used to attach a callback that will be invoked when the promise is fulfilled. It takes a single argument, a function that will be passed the result of any asynchronous operations.

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

Furthermore, the .catch() method is used to attach a callback that will be invoked when the promise is rejected. It takes a single argument, a function that will be passed the error.

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

Promise Chaining

Promise chaining is a technique that allows you to chain multiple .then() and .catch() methods together to handle the results of multiple asynchronous operations. It's worth mentioning that promise chaining can improve the readability and maintainability of the code.

promise
  .then((result) => {
    // Do something with the result
    return new Promise((resolve) => {
      resolve(result);
    });
  })
  .then((result) => {
    // Do something else with the result
    console.log(result);
  })
  .catch((error) => {
    console.log(error);
  });

Promisification

Simply put, promistification is transforming a callback-accepting function into a function that returns a promise. Sometimes, this can be useful when working with older libraries that were written before Promises were introduced.

function callbackFunction(arg, callback) {
  // Asynchronous operation
  callback(result);
}

const promisifiedFunction = (arg) => {
  return new Promise((resolve, reject) => {
    callbackFunction(arg, (result) => {
      resolve(result);
    });
  });
};

In the above example, we have a callback-based function called callbackFunction which takes two arguments: arg and callback. The function performs an asynchronous operation and when it's done, it calls the callback function passing the result as the argument.

To make the function promise-based, we wrapped it inside a new promise object. Then, we pass the arg to the callbackFunction as before, but instead of passing the callback, we use the resolve function of the promise to return the result.

const promisifiedFunction = (arg) => {
  return new Promise(async (resolve, reject) => {
    try {
      const result = await callbackFunction(arg);
      resolve(result);
    } catch (error) {
      reject(error);
    }
  });
};

Another option is to use the async/await syntax to handle the promise. The promisifiedFunction is now declared as async, and we are using the await keyword to wait for the callbackFunction to finish. We're also using a try-catch block to handle potential errors. If everything goes well, the result will be passed to the resolve function, otherwise, it will be passed to the reject function.

Learn more about async/await in our JavaScript async/await article.

Promise Methods

We already mentioned some of the methods in the examples above. Promise objects in JavaScript come with several built-in methods that allow you to handle different scenarios and situations when working with promises. Here's a list of the most commonly used methods:

// .then() method
promise.then((value) => {
  console.log(`Promise resolved with value: ${value}`);
}, (error) => {
  console.log(`Promise rejected with reason: ${error}`);
});

// .catch() method
promise.catch((error) => {
  console.log(`Promise rejected with reason: ${error}`);
});

//Promise.all() method
Promise.all([promise1, promise2, promise3])
  .then((results) => {
    console.log(`Promise.all: ${results}`);
  })
  .catch((error) => {
    console.log(`Promise.all: ${error}`);
  });

//Promise.race() method
Promise.race([promise1, promise2, promise3])
  .then((firstResult) => {
    console.log(`Promise.race: ${firstResult}`);
  })
  .catch((error) => {
    console.log(`Promise.race: ${error}`);
  });

//Promise.resolve() method
Promise.resolve('Hello World')
  .then((result) => {
    console.log(`Promise.resolve: ${result}`);
  });

//Promise.reject() method
Promise.reject(new Error('Something went wrong'))
  .catch((error) => {
    console.log(`Promise.reject: ${error}`);
  });

//Promise.prototype.finally() method
promise
  .then((value) => {
    console.log(`Promise resolved with value: ${value}`);
  })
  .catch((error) => {
    console.log(`Promise rejected with reason: ${error}`);
  })
  .finally(() => {
    console.log('Promise settled');
  });

Common Uses in Modern Web Applications

Promises are widely used in modern web applications to handle asynchronous operations such as network requests, user input, and timers. Here are a few examples of how promises can be used in web applications:

// Fetching data from an API
fetch('https://jsonplaceholder.typicode.com/todos/1')
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
  })
  .then((data) => {
    console.log(`Fetched todo: ${data.title}`);
  })
  .catch((error) => {
    console.log(`Error: ${error}`);
  });

In the example, we use the Fetch API which returns a promise that resolves with a Response object. We can then use the .then() method to handle the response, check if it's ok, and parse the JSON data. If there's an error, it will be caught by the .catch() method.

// Uploading a file
const input = document.querySelector('input[type="file"]');
const formData = new FormData();

input.addEventListener('change', (event) => {
  const file = event.target.files[0];
  formData.append('file', file);

  fetch('/upload', {
    method: 'POST',
    body: formData
  })
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
  })
  .then((data) => {
    console.log(`File uploaded successfully: ${data.fileName}`);
  })
  .catch((error) => {
    console.log(`Error: ${error}`);
  });
});

We use the DOM API to handle a file input and the Fetch API to upload the file to the server. It listens for a change event on the file input, gets the selected file, appends it to a FormData object, and sends it to the server via a POST request. If the server responds with a success status, it logs a success message. If there's an error, it will be caught by the .catch() method.

Conclusion

  • Promises provide a more elegant and readable way to handle asynchronous code in JavaScript.

  • They provide a way to handle errors and allow for promise chaining to handle multiple asynchronous operations.

  • Promisification can be used to convert callback-based functions to promise-based functions.

  • The Promise object provides several useful methods to handle promises more efficiently.

    • The .then() method allows you to handle the results of a resolved promise, as well as catch the rejection of a rejected promise.

    • The .catch() method is a shorthand for a rejected promise and allows you to handle the rejection of a rejected promise.

    • The Promise.all() method allows you to handle the results of multiple resolved promises and the first rejected promise.

    • The Promise.race() method allows you to handle the results of the first resolved promise or the first rejected promise.

    • The Promise.resolve() method allows you to convert a non-promise value into a resolved promise.

    • The Promise.reject() method allows you to create a rejected promise with a specific reason.

  • Common uses of promises in modern web applications include handling responses from an API, data storage, and user input.

  • For certain situations, the async/await syntax is also a possible option to handle promises.