简体   繁体   中英

Exception gets swallowed in promise chain

I noticed something very strange happening when an exception is thrown inside a chain of promises in Parse for React Native. The promise chain never resolves, and never rejects, and the exception is never thrown. It just disappears silently.

Here's sample code to recreate the problem:

// Replacing this with Promise.resolve() prints the error.
// Removing this stage prints the error.
Parse.Promise.as()
  // Removing this stage causes a red screen error.
  .then(function() {
    // Replacing this with Parse.Promise.as() causes a red screen error.
    return Promise.resolve();
  })
  .then(function () {
    throw new Error("There was a failure");
  })
  .then(function () { console.log("Success")}, function (err) { console.log(err) });

As you can see from the comments, it only seems to happen in this particular sequence of events. Removing a stage, or swapping a Parse promise for a native JS promise, causes things to behave again. (In my actual code, the "Promise.resolve()" stage is actually a call into a native iOS method that returns a promise.)

I'm aware that Parse promises don't behave exactly like A+ compliant promises (cf. https://stackoverflow.com/a/31223217/2397068 ). Indeed, calling Parse.Promise.enableAPlusCompliant() before this section of code causes the exception to be caught and printed. But I thought that Parse promises and native JS promises could be used together safely.

Why is this exception disappearing silently?

Thank you.

For your consideration in addition to technical reasons provided in your quoted answer:

Compliant promises

ES6/A+ compliant Promise instances share:

  1. When a promise is settled, its settled state and value are immutable.
  2. A promise cannot be fulfilled with a promise.
  3. A then registered 'fulfill' listener is never called with a promise (or other thenable) object as argument.
  4. Listeners registered by then are executed asynchronously in their own thread after the code which caused them to be executed has run to completion.
  5. Irrespective of whether a listener was registered for call back whan a promise becomes settled ( 'fulfilled' or 'rejected'),

    • the return value of a listener is used to fulfill resolve the promise returned by its then registration
    • a value thrown (using throw ) by a listener is used to reject the promise returned by then registration, and
  6. A promise resolved with a Promise instance synchronizes itself with the eventual settled state and value of the promise provided as argument. (In the ES6 standard this is described as "locked in").

  7. A promise resolved with a thenable object which is not a Promise instance will skip synch as required: if at first resolved with a thenable which "fulfills" itself with a thenable, the Promise promise will re-synchronize itself with the most recent 'thenable' provided. Skipping to a new thenable to synchronize with cannot occur with A+ promises because they never call a "fulfilled" listener with a thenable object.

Non Compliant promises

Potential characteristics of non compliant promise like objects include

  • allowing the settled state of a promise to be changed,
  • calling a then 'fulfilled' listener with a promise argument,
  • calling a then listener, either for 'fulfilled' or 'rejected' states, synchronously from code which resolves or rejects a promise,
  • not rejecting a then returned promise after a listener registered in the then call throws an exception.

Interoperability

Promise.resolve can be used to settle its returned promise with a static value, perhaps mainly used in testing. Its chief purpose, however, is to quarantine side effects of non compliant promises. A promise returned by Promise.resolve( thenable) will exhibit all of behaviours 1-7 above, and none of the non compliant behaviours.

IMHO I would suggest only using non A+ promise objects in the environment and in accordance with documentation for the library which created them. A non compliant thenable can be used to to resolve an A+ promise directly (which is what Promise.resolve does), but for full predictability of behaviour it should be wrapped in a Promise object using Promise.resolve( thenable) before any other use.

Note I tried to test Parse promises for A+ compliance but it does not seem to provide a constructor. This makes claims of "almost" or "fully" A+ compliance difficult to quantify. Thanks to zangw for pointing out the possibility of returning a rejected promise form a listener as an alternative to throwing an exception.

Why is this exception disappearing?

Parse does by default not catch exceptions, and promises do swallow them.

Parse is not 100% Promises/A+ compatible, but nonetheless it does try to assimilate thenables that are returned from then callbacks. And it does neither catch exceptions nor executes its own callbacks asynchronously.

What you are doing can be reproduced without then using

var p1 = new Parse.Promise();
var p2 = new Parse.Promise();

// p2 should eventually settle and call these:
p2._resolvedCallbacks = [function (res) { console.log("Success") }];
p2._rejectedCallbacks = [function (err) { console.log(err) }];

function handler() {
    throw new Error("There was a failure");
}
// this is what the second `then` call sets up (much simplified):
p1._resolvedCallbacks = [function(result) {
    p2.resolve(handler(result)); // throws - oops
}];

// the native promise:
var p = Promise.resolve();
// what happens when a callback result is assimilated:
if (isThenable(p))
    p.then(function(result) {
        p1.resolve(result);
    });

The problem is that p1.resolve is synchronous, and executes the callbacks on p1 immediately - which in turn does throw. By throwing before p2.resolve can be called, p2 will stay forever pending. The exceptions bubbles up and becomes the completion of p1.resolve() - which now throws in a callback to a native then method. The native promise implementation catches the exception and rejects the promise returned by then with it, which is however ignored everywhere.

silently?

If your "native" promise implementation supports unhandled rejection warnings, you should be able to see the exception hanging around in the rejected promise.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM