8

If a Promise p is resolved with the value of a Promise (or Thenable) q, it essentially becomes a copy of Promise q. If q is resolved, p will be resolved with the same value.

Promise.resolve(Promise.resolve("hello"));
Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: "hello"}

If q is rejected, p will be rejected with the same value.

Promise.resolve(Promise.reject(new Error("goodbye")));
Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: Error: goodbye}

The fact that Promise p was resolved/rejected through Promise q, instead of directly with the respective value, is irrelevant to the final result. The intermediate Promise is consumed as part of the resolution process, and is not visible to the consumer.

If q is a never resolved or rejected, p will also remain pending forever.

Promise.resolve(new Promise(() => null)); // perpetually-pending promise
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}

These cases are well-known, but I have never seen what happens if a Promise is rejected (instead of resolved) with another Promise value. Does the rejection process also consume intermediate Promises, or are they passed through intact?

If it does consume them, how does that work?

Jeremy
  • 1
  • 85
  • 340
  • 366
  • This isn't difficult to test yourself, but I was trying to quickly look up this behaviour the other day and couldn't find the answer in my searches, so I decided to make a post. – Jeremy Aug 29 '16 at 02:33
  • Not certain what Question is? – guest271314 Aug 29 '16 at 02:37
  • I'm trying to clarify whether the Promise/async-nature of the arguments to resolve() and reject() is handled differently (yes). I'm sure there are formal term that would better-describe what I'm trying to say (monadicness?), but I'm not sure of them. – Jeremy Aug 29 '16 at 02:42
  • Is the issue the `Uncaught (in promise) Promise` error? Not certain what issue you are trying to solve? – guest271314 Aug 29 '16 at 02:48
  • 1
    @guest271314: No. There are three sentences with a question mark in the post. Read them. There is no issue, he is just trying to understand what happens. – Bergi Aug 29 '16 at 02:48
  • 3
    "*I have never seen what happens*" - for good reason. [A Promise.reject message should always be wrapped in an Error](http://stackoverflow.com/q/26020578/1048572). – Bergi Aug 29 '16 at 02:50
  • @Bergi A good point. I found myself asking this question now when cleaning up an old code base that was widely violating that guideline. – Jeremy Aug 29 '16 at 02:51
  • 1
    "If q is **resolved**, p will be **resolved** with the same value." is a trap! It should read "If q is **fulfilled**, p will be **fulfilled** with the same value". – traktor Aug 29 '16 at 05:28

4 Answers4

10

Let's see what happens if we reject a Promise p with a resolved Promise q:

Promise.reject(Promise.resolve("hello"));
Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: Promise}
Uncaught (in promise) Promise {
    [[PromiseStatus]]: "resolved", [[PromiseValue]]: "hello"}

Or more explicitly:

const q = Promise.resolve("hello");
const p = Promise.reject(q);
p.then(null, x => console.log("Rejection value:", x));
Rejection value: Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: "hello"}

The Promise q, the rejection value, is never unwrapped! p's rejection handlers are called with the the Promise q itself, not the value it contains.

This also means that p's rejection handler doesn't need to wait for q to be resolved before it can run. Even if q is never resolved, p's rejection handler can still be called.

Promise.reject(new Promise(() => null)); // Reject with perpetually-pending Promise
Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: Promise}
Uncaught (in promise) Promise {
    [[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}

Finally, let's confirm the behaviour if we reject Promise p using another a rejected Promise q:

Promise.reject(Promise.reject(new Error("goodbye")));
Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: Promise}
Uncaught (in promise) Error: goodbye(…)(anonymous function)
Uncaught (in promise) Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: Error: goodbye}

We see again that q is not unwrapped, and p's rejection handler will be called with q itself, not the value that q has been rejected with.

Jeremy
  • 1
  • 85
  • 340
  • 366
  • Not certain what Answer demonstrates? – guest271314 Aug 29 '16 at 02:38
  • 3
    @guest271314 This answer demonstrates the answer to the question asked. – zerkms Aug 29 '16 at 02:39
  • @zerkms Not certain what the Question is? – guest271314 Aug 29 '16 at 02:39
  • 3
    @guest271314 perhaps. If you cannot understand it - please ask another question to explain this one for you. (we need to go deeper) – zerkms Aug 29 '16 at 02:40
  • @zerkms Ok. Though not sure what expected result is? What is the issue with `javascript` at Question which present Answer resolves? – guest271314 Aug 29 '16 at 02:42
  • I'm sorry, I don't think comments is the suitable place to explain the whole promises idea. Please google for some articles and read MDN. – zerkms Aug 29 '16 at 02:43
  • 1
    @guest271314 - The conclusion here is that giving a rejected promise to `Promise.reject()` just passes that rejected promise through as is. So, the reject `reason` that any `.catch()` handler will see will be the actual rejected promise as the `reason`. This appears to be different than `Promise.resolve()` which will unwrap any promise you give it. – jfriend00 Aug 29 '16 at 04:01
  • @jfriend00 Still trying to interpret correctly. What do you mean by "unwrap" here? – guest271314 Aug 29 '16 at 04:09
  • 1
    @guest271314 - Unwrap means that it doesn't just return the promise, but rather sees you're specifying a promise so it looks inside of that promise and uses that promise's value. A common example of unwrapping is when you return a promise from a `.then()` handler. The `.then()` handler doesn't just make that promise by the resolved value of the parent promise. Instead, it takes that promise and chains it onto the parent promise. This is often referred to as unwrapping. – jfriend00 Aug 29 '16 at 04:14
  • @jfriend00 Ok. Got it. Tried both `var q = Promise.reject(new Error("hello")); var p = Promise.resolve(q);` , `var q = Promise.resolve("hello"); var p = Promise.reject(new Error(q));` able to view different results – guest271314 Aug 29 '16 at 04:23
  • 1
    @guest271314 - Yeah, I did something similar here: https://jsfiddle.net/jfriend00/xbqp2toj/ – jfriend00 Aug 29 '16 at 04:28
  • @jfriend00 This is expected behaviour? – guest271314 Aug 29 '16 at 04:30
  • @guest271314 - I haven't read the spec on this topic, but four promise implementations all do it the same way: Bluebird, Chrome, Firefox and Edge. – jfriend00 Aug 29 '16 at 04:32
9

So, Jeremy's answer explains what happens:

const p = Promise.reject(Promise.resolve(3)); 

p is a rejected promise with the rejection value of a Promise of 3.

We were taught to believe promises never resolve with promises! Well, this is a special case. Here, we are rejecting a promise with another promise in contradiction to what then does.

But why?!?

Easy there sport. Let's first get some terminology down.

A promise starts of as pending, it can either become:

  • fulfilled - marking it completed with a value.
  • rejected - marking it failed with a reason.

So far so good, but let's consider two additional terms:

  • resolved - meaning it resolved to another promise value and is tracking it.
  • settled - meaning it's actually fulfilled or rejected - either through the promise it's following resolving or on its own.

Phew. Now that that's out of the way:

What Promise.resolve does is create a promise resolved to another value. If that value is a promise it tracks it - otherwise it settles immediately with the value passed in. This is also what happens if you return from within a then or await something in an async function.

What Promise.reject does is create a promise rejected with another value. It has no chance to follow another promise as it is immediately created with a rejected result.

This behavior is specified in reject and resolve. In particular - we're creating a promise capability and resolve is special - namely look at "Promise Resolve Functions".

Ok, you told me what happens - but why?!?!?!?

Well, let's consider the alternatives. We want resolve to mimic returning from a then or awaiting in an async function and reject to mimic throwing in a then or in an async function.

const p = Promise.resolve().then(() => {
    throw Promise.reject(5);
});

It is clearer to see resolving p to 5 makes no sense! We'd mark the promise as completed correctly but it clearly did not complete correctly.

Similarly:

async function foo() { throw Promise.resolve(5); } foo(); // no one would expect foo to resolve.

What about rejecting with the unwrapped value?

That would mean we lose the information about which rejection we're dealing with. The only alternative is to reject with a Promise object.

Should I ever run into this?

No, never. You should never throw promises anyway and you should always reject with Errors.

Community
  • 1
  • 1
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • "we lose the information about which rejection we're dealing with" — I'm not sure I follow what you mean, or perhaps why this matters. In the fulfillment case, it doesn't matter how deeply nested the ultimate value is, it is still unwrapped (tracked to be more accurate) as the actual value of interest. Why would you want rejection to follow different logic? The only thing I can think of is that `throw` ought to cause rejection even if you `throw` a *pending* promise; i.e. it's ok for a normal promise to never settle, but a `throw` should activate error handling no matter what is thrown. – Gabriel L. Dec 27 '16 at 06:53
  • 1
    @GabrielL. because rejections indicate errors where completion values are just values. An error means there might be a bug with your code at which point you _really_ want all the errors and a stack trace on each one. – Benjamin Gruenbaum Dec 27 '16 at 08:54
  • 1
    Ah, that makes perfect sense. As most aspects of promises do when you relate them to the synchronous try-catch-finally behavior they seek to emulate. Thanks! …PS and yes, always `throw` with errors, absolutely. I asked about this corner case because I wrote an A+ compliant promise lib for fun, but now am taking the extra step of matching ES promises. – Gabriel L. Dec 27 '16 at 09:10
4

Promise object construction:

    new Promise( executor)

calls the executor function with two call back function arguments:

   executor( resolve, reject)

where resolve is overloaded by argument type to either

  1. link the promise to a thenable:

    resolve( thenable);  // link the resolved promise to the final state of the thenable.
    

    where the resolved promise remains pending until the promise it is linked to ("was resolved with") becomes settled, or

  2. fulfill the promise with something that is not a thenable

      resolve (notThenable);  // fulfill the promise with anything except a thenable.
    

Being javascript, "overloaded" in this context is performed by examination of the argument type and properties by resolve at run time, not when the script is compiled. A simplified explanation is that you can't fulfill a promise with a promise or promise like object.

The reject function is not overloaded and does not examine its argument at run time. If a promise is rejected with a promise or other thenable it is treated as any other rejection reason. The promise does not remain pending and becomes rejected. The promise used for rejection is passed as the reason argument to any functions catching promise rejection. A simplified explanation is that you can reject a promise with anything you like, but if it is not an Error object or descriptive reason you are on your own!

traktor
  • 17,588
  • 4
  • 32
  • 53
-1

Promise.resolve() can take a value, a thenable or a promise. It adapts its behaviour.

Promise.reject() takes only an immediate value. So if you pass a Promise to it, it will clumsily try to treat it as an immediate value.

However, you do not consume the promise by passing it to Promise.reject. You can do this:

Promise.reject(myPromise); // weird and useless, and with no side effect
myPromise.then(e=>{console.log(e)}); // "consume" the promise
myPromise.then(e=>{console.log(e)}); // you can consume it as many times as you want
solendil
  • 8,432
  • 4
  • 28
  • 29