5

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.

Community
  • 1
  • 1
Lane Rettig
  • 6,640
  • 5
  • 42
  • 51
  • 1
    You may have answered your own question. @Bergi 's [accepted answer](http://stackoverflow.com/a/31223217/2397068) indicates Parse promises are not A+ compliant **by default** and the first code review point says that after making them compliant "Exceptions in then callbacks are caught and lead to the rejection of the result promise, instead of a global error". It would seem you can use Parse and JS promises together **provided** you make Parse promises compliant first. – traktor Jan 18 '16 at 00:21
  • `Parse.Promise.as(true).then(function() { return Parse.Promise.error("here is an error"); }).then(function(done) { console.log('done', done); }, function(err) { console.log('err', err); });` also could catch the error.. – zangw Jan 18 '16 at 01:10
  • @zangw thanks for the Parse native alternative. I've tried to explain why Promise.resolve cures a lot of things below :-) – traktor Jan 18 '16 at 06:44
  • @Traktor53 I understand that, when they are made A+ compliant, then "Exceptions... are caught, instead of a global error," but the issue I reported here involves exceptions vanishing completely! I.e. there was no global error. – Lane Rettig Jan 19 '16 at 00:21
  • @zangw thanks for the helpful tip! Unfortunately in my actual code, the exception is being thrown in third party code and I can't change this to a rejected promise instead (without wrapping it in a catch block, or nesting another promise, which to me defeats the purpose of chaining promises in the first place!). – Lane Rettig Jan 19 '16 at 00:25

2 Answers2

0

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.

traktor
  • 17,588
  • 4
  • 32
  • 53
  • Hi @Traktor53, I'm curious what you mean about Parse promises not having a constructor. You can just call `new Parse.Promise()` to get one. This is a constructor, isn't it? https://github.com/ParsePlatform/Parse-SDK-JS/blob/master/src/ParsePromise.js#L29 – Lane Rettig Jan 27 '16 at 18:02
  • You are absolutely correct - strangely constructor usage is not mentioned in the [website API documention](https://parse.com/docs/js/api/classes/Parse.Promise.html) but using `Parse.Promise` as a constructor is covered in the [developers' guide](https://parse.com/docs/js/guide#promises). – traktor Jan 27 '16 at 22:04
0

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.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thanks for the detailed response. How do you recommend avoiding this issue in future? Use A+ compatible promises? Add a .catch block at the end? (cf. "Rookie mistake #3: forgetting to add .catch()" from http://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html) Also, do I need a third party promise lib like bluebird to get "unhandled rejection warnings"? – Lane Rettig Jan 19 '16 at 00:54
  • I'd recommend to use `enableAPlusCompliant()`, yes. The `.catch` call won't help if your promises aren't catching exceptions. I don't think you'd need a third party promise library, afaik both FF and Chrome native promises do support unhandled rejection tracking (maybe you need to enable it in the devtools). Of course, using Bluebird is never a mistake :-) – Bergi Jan 19 '16 at 01:23
  • Thanks. I've enabled A+ compliance and so far so good, although I have found at least one relatively high profile bug that this introduces (https://github.com/ParsePlatform/ParseReact/issues/161), which makes me wonder how many more are lurking. – Lane Rettig Jan 21 '16 at 15:15