Categories
JavaScript

Promise


Description

Promise is a proxy for a value not necessarily known when the promise is created. 

The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

Instead of callback functions, it allows you to associate handlers with an asynchronous action’s eventual success value or failure reason.

This lets asynchronous methods return values like synchronous methods: instead of immediately returning the final value, the asynchronous method returns a promise to supply the value at some point in the future.

Promise is in one of these states:

  • pending: initial state, neither fulfilled nor rejected.
  • fulfilled: meaning that the operation was completed successfully.
  • rejected: meaning that the operation failed.

Chaining

const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);

Here’s the magic: the then() function returns a new promise, different from the original.

This second promise (promise2) represents the completion not just of doSomething(), but also of the successCallback or failureCallback you passed in — which can be other asynchronous functions returning a promise.

doSomething()
  .then((result) => doSomethingElse(result))
  .then((newResult) => doThirdThing(newResult))
  .then((finalResult) => {
    console.log(`Got the final result: ${finalResult}`);
  })
  .catch(failureCallback);

Important: Always return results, otherwise callbacks won’t catch the result of a previous promise. If the previous handler started a promise but did not return it, there’s no way to track its settlement anymore, and the promise is said to be “floating”.


Nesting

doSomethingCritical()
  .then((result) =>
    doSomethingOptional(result)
      .then((optionalResult) => doSomethingExtraNice(optionalResult))
      .catch((e) => {}),
  ) // Ignore if optional stuff fails; proceed.
  .then(() => moreCriticalStuff())
  .catch((e) => console.error(`Critical failure: ${e.message}`));

Nesting is a control structure to limit the scope of catch statements. Specifically, a nested catch only catches failures in its scope and below, not errors higher up in the chain outside the nested scope. 


Common mistakes

doSomething()
  .then(function (result) {
    // Forgot to return promise from inner chain + unnecessary nesting
    doSomethingElse(result).then((newResult) => doThirdThing(newResult));
  })
  .then(() => doFourthThing());
// Forgot to terminate chain with a catch!


// Correct version:
doSomething()
  .then(function (result) {
    // If using a full function expression: return the promise
    return doSomethingElse(result);
  })
  // If using arrow functions: omit the braces and implicitly return the result
  .then((newResult) => doThirdThing(newResult))
  // Even if the previous chained promise returns a result, the next one
  // doesn't necessarily have to use it. You can pass a handler that doesn't
  // consume any result.
  .then((/* result ignored */) => doFourthThing())
  // Always end the promise chain with a catch handler to avoid any
  // unhandled rejections!
  .catch((error) => console.error(error));

Rejection Events

If a promise rejection event is not handled by any handler, it bubbles to the top of the call stack, and the host needs to surface it. On the web, whenever a promise is rejected, one of two events is sent to the global scope (generally, this is either the window or, if being used in a web worker, it’s the Worker or other worker-based interface). 

unhandledrejection

Sent when a promise is rejected but there is no rejection handler available.

rejectionhandled

Sent when a handler is attached to a rejected promise that has already caused an unhandledrejection event. (?)

// In Node.js
process.on("unhandledRejection", (reason, promise) => {
  // Add code here to examine the "promise" and "reason" values
});

Timing

Note that promises are guaranteed to be asynchronous. Therefore, an action for an already “settled” promise will occur only after the stack has cleared and a clock-tick has passed.

Functions passed to then() will never be called synchronously, even with an already-resolved promise: (?)

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

wait(0).then(() => console.log(4));
Promise.resolve()
  .then(() => console.log(2))
  .then(() => console.log(3));
console.log(1); 

// Output: 1, 2, 3, 4

Concurrency

Promise.all()

Fulfills when all of the promises fulfill; rejects when any of the promises rejects.

Promise.allSettled()

Fulfills when all promises settle.

Promise.any()

Fulfills when any of the promises fulfills; rejects when all of the promises reject.

Promise.race()

Settles when any of the promises settles. In other words, fulfills when any of the promises fulfills; rejects when any of the promises rejects.

All these methods take an iterable of promises and return a new promise.

Difference between all() and allSettled():

Promise.all will reject as soon as one of the Promises in the array rejects.

Promise.allSettled will never reject – it will resolve once all Promises in the array have either rejected or resolved.


Sequential composition

[func1, func2, func3]
  .reduce((p, f) => p.then(f), Promise.resolve())
  .then((result3) => {
    /* use result3 */
  });

Instance Method

Promise.prototype.finally()

Appends a handler to the promise, and returns a new promise that is resolved when the original promise is resolved. The handler is called when the promise is settled, whether fulfilled or rejected.


Refs:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

Leave a comment