JavaScript promises

A JavaScript promise is an object that represents the eventual completion (or failure) of an asynchronous operation, and its resulting value. Promises were introduced in JavaScript to provide a more efficient and elegant way of handling asynchronous operations, such as network requests or reading from a file.

The principles of promises

  • A promise is in one of three states: pending, fulfilled, or rejected.
  • A promise can only transition from pending to fulfilled or rejected, and once it has done so, its state cannot change.
  • A promise can have callbacks (often referred to as "handlers") attached to it, which will be called when the promise is fulfilled or rejected. These callbacks are known as "then" and "catch" handlers, respectively.

The three states of a promise

  • Pending: Initial state, neither fullled nor rejected.
  • Fullled: The operation was completed successfully.
  • Rejected: The operation failed.

Best practices for using promises

  • Always returning a promise from an async function, so that the caller can easily handle the result of the async operation.
  • Chaining multiple promises together, rather than nesting them, to avoid "callback hell".
  • Handling errors and rejections in a consistent and predictable way.

The pros of JavaScript promises

  • Improved readability and maintainability: Promises make it easier to handle asynchronous operations by providing a clear and consistent way of handling success and failure. This can make code easier to understand and debug, compared to using callbacks.
  • Better error handling: Promises provide a way to handle errors in a centralized and consistent way, using the "catch" handler. This can make it easier to detect and fix bugs in your code.
  • Chaining and composition: Promises can be easily chained together, making it easy to perform multiple asynchronous operations in a specific order. This can make it easy to write code that is expressive and easy to understand.
  • Better performance: Promises can be used to perform multiple asynchronous operations in parallel, which can improve the performance of your code.

The cons of JavaScript promises

  • Steep learning curve: The concepts behind promises can be difficult to understand, especially for developers who are new to asynchronous programming.
  • Verbosity: Promises can make your code more verbose, as you need to handle both the success and failure cases separately.
  • Limited control over execution order: Once a promise is fulfilled or rejected, it cannot be changed. This can make it difficult to handle certain situations where you need more control over the execution order of your code.
  • Limited browser support: Promises were introduced in ECMAScript 6 and may not be supported in older browsers. This means that you may need to include a polyfill to ensure that your code works on all platforms.
  • Verbosity: Promises can make your code more verbose, as you need to handle both the success and failure cases separately.
  • Limited control over execution order: Once a promise is fulfilled or rejected, it cannot be changed. This can make it difficult to handle certain situations where you need more control over the execution order of your code.
  • Limited browser support: Promises were introduced in ECMAScript 6 and may not be supported in older browsers. This means that you may need to include a polyfill to ensure that your code works on all platforms.

There are several methods available on JavaScript promises that can be used to handle the results of an asynchronous operation

Promise

Creates a new Promise object. The constructor is primarily used to wrap functions that do not already support promises.

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

Promise.prototype.then()

The .then(onFulfilled, onRejected) method of a JavaScript Promise object can be used to get the eventual result of the asynchronous operation.

This method attaches callbacks to the promise that will be called when the promise is fulfilled or rejected. The onFulfilled callback will be called with the result of the promise, and the onRejected callback will be called with the reason for the rejection.

Usage:

asyncOperation().then(result => console.log(result));

Here is an example:

fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));

In this example, the first "then" handler transforms the response into JSON and the second "then" handler logs the data. If an error occurs at any point in this chain, it will be caught by the "catch" handler.

Promise.prototype.catch()

The information for the rejection of the promise is available to the handler supplied in the .catch(onRejected) method.

The method attaches a callback that will be called when the promise is rejected. This is a shorthand for promise.then(null, onRejected).

Usage:

asyncOperation().catch(err => console.log(err));

Here is an example:

fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));

In this example, the catch handler logs the error if the fetch promise rejected.

Promise.prototype.finally()

The handler is called when the promise is settled, whether fullfilled or rejected.

The finally(onFinally) attaches a callback that will be called when the promise is fulfilled or rejected, regardless of the outcome. This can be used to perform cleanup or other tasks that need to be done after the promise has been resolved.

Usage:

asyncOperation().finally(() => console.log('async operation ended!'));

Here is an example:

fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error))
  .finally(() => console.log('fetch completed'));

In this example, the finally handler logs that the fetch is completed regardless of whether it succeeded or failed.

Promise.resolve()

Returns a promise that resolves to the value given to it.

The Promise.resolve(value) creates a new promise that is fulfilled with the given value. This can be useful for wrapping non-promise values in a promise.

Usage:

Promise.resolve(15).then(console.log);

Here is an example:

Promise.resolve(42).then(value => console.log(value))

In this example, the promise is fulfilled with the value 42 and then handler logs 42.

Promise.reject()

Returns a promise that rejects with an error given to it.

The Promise.reject(reason) creates a new promise that is rejected with the given reason. This can be useful for creating a rejected promise with a specific error message.

Usage:

Promise
.reject(new Error('This is an error!'))
.catch(console.log);

Here is an example:

Promise.reject(new Error('Something went wrong')).catch(error => console.error(error))

In this example, the promise is rejected with the error 'Something went wrong' and catch handler logs the error.

Promise.all([…promises])

Wait for all promises to be resolved, or for any to be rejected.

The Promise.all([promise1, promise2, ...]) creates a new promise that is fulfilled with an array of the fulfilled values of the input promises, in the same order as the input promises. If any of the input promises are rejected, the returned promise is also rejected. This can be used to perform multiple asynchronous operations in parallel.

Usage:

Promise
.all([promise1, promise2])
.then(([val1, val2]) => console.log(val1, val2));

Here is an example:

Promise.all([
    fetch('https://api.example.com/data1'),
    fetch('https://api.example.com/data2'),

Promise.allSettled([…promises])

Wait until all promises have settled (each may resolve or reject).

The Promise.allSettled(promises) method creates a new promise that is fulfilled with an array of objects describing the outcome of the input promises. Each object has a status property, which is either "fulfilled" or "rejected", and a value or reason property, which contains the result or error of the corresponding promise. This can be used to handle the results of multiple promises, regardless of whether they succeed or fail.

Usage:

Promise
.allSettled([promise1, promise2])
.then(results => {
results.forEach(result => console.log(result.status));
});

Here is an example:

Promise.allSettled([
    fetch('https://api.example.com/data1'),
    fetch('https://api.example.com/data2'),
    fetch('https://api.example.com/data3')
]).then(results => {
    results.forEach(result => {
        if (result.status === 'fulfilled') {
          console.log(`Fulfilled: ${result.value}`);
        } else {
          console.error(`Rejected: ${result.reason}`);
        }
    });
});

In this example, it's fetching three data in parallel and once all promises are resolved, then handler is called with the results of all the promises.

Promise.any([…promises])

Takes an iterable of Promise objects and as soon as one of the promises in the iterable fullls, returns a single promise that resolves with the value from that promise.

The method Promise.any(promises) creates a new promise that is fulfilled with the first input promise to be fulfilled. If all input promises are rejected, the returned promise is also rejected. This can be used to handle the first resolved promise among many promises.

Usage:

Promise
.any([promise1, promise2])
.then(value => console.log(value));

Here is an example:

Promise.any([
    fetch('https://api.example.com/data1'),
    fetch('https://api.example.com/data2'),
    fetch('https://api.example.com/data3')
]).then(value => console.log(value))
  .catch(error => console.error(error));

In this example, it's fetching three data in parallel and once any promise is resolved, then handler is called with the value of the resolved promise, if all promises are rejected, catch handler is called.

Promise.race([…promises])

Wait until any of the promises is resolved or rejected. The dierence with .any is that the outer promise can be rejected if an internal promise gets rejected.

The method Promise.race(promises) creates a new promise that is fulfilled or rejected with the same outcome as the first input promise to be fulfilled or rejected. This can be used to handle the first resolved or rejected promise among many promises.

Usage:

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

Here is an example:

Promise.race([
    fetch('https://api.example.com/data1'),
    fetch('https://api.example.com/data2'),
    fetch('https://api.example.com/data3')
]).then(value => console.log(value))
  .catch(error => console.error(error));

In this example, it's fetching three data in parallel and once any promise is resolved or rejected, then or catch handler is called with the value or error of the first resolved or rejected promise.


Links