Skip to content

Latest commit

 

History

History
529 lines (362 loc) · 22.4 KB

promise.md

File metadata and controls

529 lines (362 loc) · 22.4 KB

Promise

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


References

Description

Basics

A Promise is a proxy for a value not necessarily known when the promise is created. 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.

A Promise is in one of these states:

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

A pending promise can either be fulfilled with a value or rejected with a reason (error). When either of these options happens, the associated handlers queued up by a promise's then method are called.

If the promise has already been fulfilled or rejected when a corresponding handler is attached, the handler will be called, so there is no race condition between an asynchronous operation completing and its handlers being attached.

As the Promise.prototype.then() and Promise.prototype.catch() methods return promises, they can be chained.

promises.png

Note: Several other languages have mechanisms for lazy evaluation and deferring a computation, which they also call "promises", e.g. Scheme ( and Future in Java ) .

Promises in JavaScript represent processes that are already happening, which can be chained with callback functions.

If you are looking to lazily evaluate an expression, consider using a function with no arguments e.g. f = () => expression to create the lazily-evaluated expression, and f() to evaluate the expression immediately.


Note:

A promise is said to be settled if it is either fulfilled or rejected, but not pending ( unsettled? ) .

You will also hear the term resolved used with promises — this means that the promise is settled or "locked-in" to match the state of another promise. States and fates contain more details about promise terminology.

Chained Promises

The methods

  • promise.then()
  • promise.catch()
  • promise.finally()

are used to associate further action with a promise that becomes settled.


The .then() method takes up to two arguments;

  • the first argument is a callback function for the resolved case of the promise, and
  • the second argument is a callback function for the rejected case.

Each .then() returns a newly generated promise object, which can optionally be used for chaining; for example:

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('foo');
  }, 300);
});

myPromise
  .then(handleResolvedA, handleRejectedA)
  .then(handleResolvedB, handleRejectedB)
  .then(handleResolvedC, handleRejectedC);

Processing continues to the next link of the chain even when a .then() lacks a callback function that returns a Promise object. Therefore, a chain can safely omit every rejection callback function until the final .catch() .

Handling a rejected promise in each .then() has consequences further down the promise chain. Sometimes there is no choice, because an error must be handled immediately. In such cases we must throw an error of some type to maintain error state down the chain. On the other hand, in the absence of an immediate need, it is simpler to leave out error handling until a final .catch() statement.

A .catch() is really just a .then() without a slot for a callback function for the case when the promise is resolved.

myPromise
    .then(handleResolvedA)
    .then(handleResolvedB)
    .then(handleResolvedC)
    .catch(handleRejectedAny);

……

The termination condition of a promise determines the "settled" state of the next promise in the chain.

A "resolved" state indicates a successful completion of the promise, while a "rejected" state indicates a lack of success.

The return value of each resolved promise in the chain is passed along to the next .then(), while the reason for rejection is passed along to the next rejection-handler function in the chain.

The promises of a chain are nested like Russian dolls, but get popped like the top of a stack. The first promise in the chain is most deeply nested and is the first to pop.

(promise D, (promise C, (promise B, (promise A))))

……

A promise can participate in more than one nesting.

For the following code, the transition of promiseA into a "settled" state will cause both instances of .then() to be invoked.

const promiseA = new Promise(myExecutorFunc);
const promiseB = promiseA.then(handleFulfilled1, handleRejected1);
const promiseC = promiseA.then(handleFulfilled2, handleRejected2);

An action can be assigned to an already "settled" promise.

In that case, the action ( if appropriate ) will be performed at the first asynchronous opportunity. 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. The effect is much like that of setTimeout(action, 10).

Constructor

Static methods

  • Promise.all(iterable)

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

    If the returned promise resolves, it is resolved with an aggregating array of the values from the resolved promises, in the same order as defined in the iterable of multiple promises.

    If it rejects, it is rejected with the reason from the first promise in the iterable that was rejected.

    icehe : 对比 allSettled(), all() 只要有其中一个 promise rejected, 就会中止并返回.

  • Promise.allSettled(iterable)

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

    Returns a Promise that resolves after all of the given promises is either fulfilled or rejected, with an array of objects that each describe the outcome of each promise.

    icehe : 对比 all(), allSettled() 要等到所有 promise 都 fullfilled 或 rejected 之后, 才会返回.

  • Promise.any(iterable)

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

    icehe : 对比 race(), anay() 只要有其中一个 promise fullfilled, 就会中止并返回.

  • Promise.race(iterable)

    Wait until any of the promises is fulfilled or rejected.

    If the returned promise resolves, it is resolved with the value of the first promise in the iterable that resolved.

    If it rejects, it is rejected with the reason from the first promise that was rejected.

    icehe : 对比 any(), race() 要等到所有 promise 都 fullfilled 或 rejected 之后, 才会返回.

  • Promise.reject(reason)

    Returns a new Promise object that is rejected with the given reason.

  • Promise.resolve(value)

    Returns a new Promise object that is resolved with the given value. If the value is a thenable (i.e. has a then method), the returned promise will "follow" that thenable, adopting its eventual state; otherwise, the returned promise will be fulfilled with the value.

    Generally, if you don't know if a value is a promise or not, Promise.resolve(value) it instead and work with the return value as a promise.

Instance methods

See the Microtask guide to learn more about how these methods use the Microtask queue and services.

  • Promise.prototype.catch()

    Appends a rejection handler callback to the promise, and returns a new promise resolving to the return value of the callback if it is called, or to its original fulfillment value if the promise is instead fulfilled.

  • Promise.prototype.then()

    Appends fulfillment and rejection handlers to the promise, and returns a new promise resolving to the return value of the called handler, or to its original settled value if the promise was not handled (i.e. if the relevant handler onFulfilled or onRejected is not a function).

  • 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.

Using Promise

Guarantees

Unlike old-fashioned passed-in callbacks, a promise comes with some guarantees:

  • Callbacks added with then() will never be invoked before the completion of the current run of the JavaScript event loop.
  • These callbacks will be invoked even if they were added after the success or failure of the asynchronous operation that the promise represents.
  • Multiple callbacks may be added by calling then() several times. They will be invoked one after another, in the order in which they were inserted.

One of the great things about using promises is chaining.

Chaining

See Chained Promises above.

……

Comparison

In the old days, doing several asynchronous operations in a row would lead to the classic callback pyramid of doom:

doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

With modern functions, we attach our callbacks to the returned promises instead, forming a promise chain:

doSomething()
.then(function(result) {
  return doSomethingElse(result);
})
.then(function(newResult) {
  return doThirdThing(newResult);
})
.then(function(finalResult) {
  console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);

The arguments to then are optional, and catch(failureCallback) is short for then(null, failureCallback).

……

Chaining after a catch

It's possible to chain after a failure, i.e. a catch, which is useful to accomplish new actions even after an action failed in the chain.

new Promise((resolve, reject) => {
    console.log('Initial');
    resolve();
})
.then(() => {
    throw new Error('Something failed');
    console.log('Do this');
})
.catch(() => {
    console.error('Do that');
})
.then(() => {
    console.log('Do this, no matter what happened before');
});

Output:

Initial
Do that
Do this, no matter what happened before

Error propagation

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

Just like above ( but not the same, here are synchronized operations )

try {
  const result = syncDoSomething();
  const newResult = syncDoSomethingElse(result);
  const finalResult = syncDoThirdThing(newResult);
  console.log(`Got the final result: ${finalResult}`);
} catch(error) {
  failureCallback(error);
}

This symmetry with asynchronous code culminates in the async/await syntactic sugar in ECMAScript 2017 :

async function foo() {
  try {
    const result = await doSomething();
    const newResult = await doSomethingElse(result);
    const finalResult = await doThirdThing(newResult);
    console.log(`Got the final result: ${finalResult}`);
  } catch(error) {
    failureCallback(error);
  }
}

Promise rejection events

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 ).

The two events are:

  • rejectionhandled

    Sent when a promise is rejected, after that rejection has been handled by the executor's reject function.

  • unhandledrejection

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

In both cases, the event (of type PromiseRejectionEvent) has as members a promise property indicating the promise that was rejected, and a reason property that provides the reason given for the promise to be rejected.

These make it possible to offer fallback error handling for promises, as well as to help debug issues with your promise management. These handlers are global per context, so all errors will go to the same event handlers, regardless of source.

One case of special usefulness: when writing code for Node.js, it's common that modules you include in your project may have unhandled rejected promises, logged to the console by the Node.js runtime. You can capture them for analysis and handling by your code —— or just to avoid having them cluttering up your output —— by adding a handler for the Node.js unhandledRejection event (notice the difference in capitalization of the name), like this:

process.on("unhandledRejection", (reason, promise) => {
  /* You might start here by adding code to examine the
   * "promise" and "reason" values. */
});

For Node.js, to prevent the error from being logged to the console ( the default action that would otherwise occur ) , adding that process.on() listener is all that's necessary; there's no need for an equivalent of the browser runtime's preventDefault() method.

However, if you add that process.on listener but don't also have code within it to handle rejected promises, they will just be dropped on the floor and silently ignored. So ideally, you should add code within that listener to examine each rejected promise and make sure it was not caused by an actual code bug.

Composition

……

Timing

To avoid surprises, functions passed to then() will never be called synchronously, even with an already-resolved promise:

Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2

Instead of running immediately, the passed-in function is put on a microtask queue, which means it runs later ( only after the function which created it exits, and when the JavaScript execution stack is empty ) , just before control is returned to the event loop; i.e. pretty soon:

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); // 1, 2, 3, 4

icehe : 以上执行顺序优先级的简短总结

  1. sync code
  2. then( plain code )
  3. then( event loop code )

Task queues vs microtasks

Promise callbacks are handled as a Microtask whereas setTimeout() callbacks are handled as Task queues.

const promise = new Promise(function(resolve, reject) {
  console.log("Promise callback");
  resolve();
}).then(function(result) {
  console.log("Promise callback (.then)");
});

setTimeout(function() {
  console.log("event-loop cycle: Promise (fulfilled)", promise)
}, 0);

console.log("Promise (pending)", promise);

The code above will output:

Promise callback
Promise (pending) Promise {<pending>}
Promise callback (.then)
event-loop cycle: Promise (fulfilled) Promise {<fulfilled>}

icehe : 暂时还理解不了, 以后回顾一下. 2021/11/17

For more details, refer to Tasks vs microtasks.

Nesting

Simple promise chains are best kept flat without nesting, as nesting can be a result of careless composition. See common mistakes below.

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.

When used correctly, this gives greater precision in error recovery:

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));

……

Common Mistakes

Here are some common mistakes to watch out for when composing promise chains. Several of these mistakes manifest in the following example:

// Bad example! Spot 3 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!
  1. The first mistake is to not chain things together properly.

    This happens when we create a new promise but forget to return it. As a consequence, the chain is broken, or rather, we have two independent chains racing. This means doFourthThing() won't wait for doSomethingElse() or doThirdThing() to finish, and will run in parallel with them, likely unintended. Separate chains also have separate error handling, leading to uncaught errors.

  2. The second mistake is to nest unnecessarily, enabling the first mistake.

    Nesting also limits the scope of inner error handlers, which —— if unintended —— can lead to uncaught errors. A variant of this is the promise constructor anti-pattern, which combines nesting with redundant use of the promise constructor to wrap code that already uses promises.

  3. The third mistake is forgetting to terminate chains with catch.

    Unterminated promise chains lead to uncaught promise rejections in most browsers.

A good rule-of-thumb is to

  • always either return or terminate promise chains, and
  • as soon as you get a new promise, return it immediately,
  • to flatten things:
doSomething()
  .then(function(result) {
    return doSomethingElse(result);
  })
  .then(newResult => doThirdThing(newResult))
  .then(() => doFourthThing())
  .catch(error => console.error(error));

……

Using async/await addresses most, if not all of these problems —— the tradeoff being that the most common mistake with that syntax is forgetting the await keyword.

When promises and tasks collide

If you run into situations in which you have promises and tasks ( such as events or callbacks ) which are firing in unpredictable orders, it's possible you may benefit from using a microtask to check status or balance out your promises when promises are created conditionally.

If you think microtasks may help solve this problem, see the microtask guide to learn more about how to use queueMicrotask() to enqueue a function as a microtask.