1

Let’s say I have a function foo which returns a promise. Is there a way to call the function, and optionally Promise.prototype.catch the result only if its rejection is unhandled? I want a solution which works in both node.js and the browser. For example:

const fooResult = foo();
// pass fooResult somewhere else where fooResult may be caught with catch
catchIfUncaught(fooResult, (err) => {
  console.log(err); // should be foo rejection only if the rejection is not caught elsewhere
  // no unhandled rejection occurs
});
brainkim
  • 902
  • 3
  • 11
  • 20
  • What do you mean by cross-environment please? If you have identified a specific environment where your code doesn't work, it would make sense to mention specifically what it is, and what goes wrong in that environment – ADyson Sep 04 '19 at 14:48
  • 1
    I mean in node.js and the browser. I know that there are global unhandled rejection events in both environments and would like a solution tht works in both environments. – brainkim Sep 04 '19 at 15:52
  • I updated the example so that `fooResult` is passed elsewhere. i want the catchIfUncaught handler to only run if `fooResult` is never caught. – brainkim Sep 04 '19 at 15:52
  • 1
    If a catch block doesn't return a rejected promise (nor throw an error), subsequent catch blocks won't be called. – sp00m Sep 04 '19 at 15:55
  • @sp00m how do we determine the order of `catch` calls? Is it based on execution order? Also can you point to the part of the spec that says this is the case? – brainkim Sep 04 '19 at 17:48

3 Answers3

1

No, there is not. When your function returns a promise, that leaves error handling to the caller - and he'll get an unhandledpromiserejection event if he misses to do that.


The only hack I can imagine would be to recognise then calls, and then cancel your own error handling:

function catchIfUncaught(promise, handler) {
    let handled = false;
    promise.catch(err => {
        if (!handled)
             handler(err);
    });
    promise.then = function(onFulfilled, onRejected) {
        handled = true;
        return Promise.prototype.then.call(this, onFulfilled, onRejected);
    };
    return promise;
}

Examples:

catchIfUncaught(Promise.reject(), err => console.log("default handler", err));

catchIfUncaught(Promise.reject(), err => console.log("default handler", err))
.catch(err => console.log("catch handler", err));

catchIfUncaught(Promise.reject(), err => console.log("default handler", err))
.then(null, err => console.log("then rejection handler", err));

catchIfUncaught(Promise.reject(), err => console.log("default handler", err))
.then(res => {})
.catch(err => console.log("chained catch handler", err));

catchIfUncaught(Promise.reject(), err => console.log("default handler", err))
.then(res => {});
// unhandled rejection (on the chained promise)

As you can see, this is only useful when the caller of your function completely ignores the result - which is really uncommon. And if he does, I'd recommend to still let the caller handle errors.


A similar hack I devised earlier would be to use the handler as the default for onRejected:

…
promise.then = function(onFulfilled, onRejected = handler) {
//                                              ^^^^^^^^^
    return Promise.prototype.then.call(this, onFulfilled, onRejected);
};

This would activate the default handler in the catchIfUncaught(…).then(res => …); case, but probably be highly counter-intuitive to the caller in longer chains.

Also notice that neither of these two hacks work properly together with await, where they always lead to an exception that the caller needs to catch. And same for any other builtin that expects a thenable - they always call .then with two arguments.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I think you might misunderstand the premise. I’m not asking if catchIfUncaught was caught, but the Promise passed to catchIfUncaught. Assume that fooResult is assigned to a property of a class or a variable in an outer scope. In my head, catchIfUncaught returns void. Essentially, I have a promise, and I want to listen for its unhandled rejection. Probably not possible, but thanks for the reply. – brainkim Sep 04 '19 at 16:44
  • 1
    @brainkim The promise passed to `catchIfUncaught` is the same as the promise it returns, to make usage easier with chaining. Works the same with `fooResult = Promise.reject(); catchIfUncaught(fooResult, …); fooResult.then(…).…`. – Bergi Sep 04 '19 at 16:47
  • Thanks, I think a solution like this will work in the end for my use-case. – brainkim Oct 14 '19 at 06:09
  • As an update, I’ve found that this hack works for node.js v10, but doesn’t work starting in v12. The problem is that await does not trigger the actual methods of native promises, so reassigning them does nothing. The solution is to create a plain old javascript object which has all the methods of a native promise, with the then and catch methods override, but not have that object be an instance of the native promise. I’ll update the example to demonstrate this if I get the chance. – brainkim Jul 30 '20 at 22:58
  • 1
    @brainkim Ah, [they changed this](https://v8.dev/blog/fast-async) and since ES10 are now using the shortcut through [*PromiseResolve*](http://www.ecma-international.org/ecma-262/10.0/#sec-promise-resolve). However, as stated in my last paragraphs, the hacks never worked with `await` anyway. Maybe you should look into [async hooks](https://nodejs.org/api/async_hooks.html) instead? – Bergi Jul 30 '20 at 23:11
  • I was able to get it all working by creating a proxy object which has the same methods of the promise but is not an instance of it. This works with await as well, because await falls back to the old thenable behavior when it encounters a non-native Promise-like object. Async hooks look interesting, but this is code that is meant for the browser, node and hopefully deno as well. – brainkim Jul 31 '20 at 18:19
  • https://es.discourse.group/t/promise-prototype-uncaught/507 – PuiMan Cheui Oct 26 '20 at 07:46
0

You could take a look to this package https://npmjs.org/package/e-promises

but you have to change your code to use the new mechanism

  1. import the EPromise
  2. extends it using YourPromise extends EPromise (optional)
  3. assign YourPromise.prototype.unchaught to your catchIfUncaught implementation
  4. change codes in foo, each place that make promises must change to use YourPromise, etc new YourPromise(executor) / YourPromise.resolve / YourPromise.all / ...
William Leung
  • 1,556
  • 17
  • 26
-1

You can just catch the error case, if you don't care about the passing case.

catchIfUncaught.catch(function (err) {
  console.error('We had an error: ', err)
})

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