Promise.prototype.uncaught

As https://tc39.es/ecma262/#sec-promise.prototype.catch stated that, Promise.prototype.catch support for catching specified rejection from a Promise. Nevertheless, developers always miss a method, which I have named as Prototype.prototype.uncaught to handle default errors not caught by any handlers.

Similarly, Node has supported unhandledRejection for developers to catch any uncaught error from a Promise.

Such a method can help developers to set up a default error handler for all promises.

1 Like

Some questions I have with this proposal:

(1) Why is it supposed to be placed on the Prototype, if it attaches a handler onto all Promises (as the unhandledRejection event handler does)?

(2) Does handling an error thrown in an async function (rejecting a Promise) differ from throwing in a sync function? Can't the handler handle the general case?

(3) What happens after the handler executed? Does execution end? Or can the handler decide? What happens if a Promise gets rejected (without a handler attached) inside the handler itself?

  1. It depends on whether you want to handle uncaught situation in a middle state or in the end? If in the middle, a prototype method is proper, while a global method is proper in the end.

  2. Take node implementation as an example, unhandledRejection will also be fired in two situations:

    process.on('unhandledRejection', () => console.log('unhandled'));
    
  • Use Promise.reject directly:

    Promise.reject(1); // fired
    new Promise((ignore, reject) => reject(1)); // fired
    
  • Two embedded async / await function:

    (async () => { await Promise.reject(1); })(); // fired
    (async () => { await (() => Promise.reject(1))(); })(); // fired
    

    For these two examples, we should also trigger 'uncaught' because developers do exactly not catch such exceptions.

  1. My assume is like the following snippet:

    Promise.reject(1).uncaught(() => { throw new Error('1 has been uncught'); }); // throw uncaught
    Promise.reject(1).catch(result => result > 1 ? Promise.reject(1) : true)
        .uncaught(() => { throw new Error('1 has been uncught'); }); // throw uncaught
    
    
    Promise.reject(1).catch(result => result > 1 ? Promise.reject(1) : true)
        .then(console.log) // => true (the reuslt has been caught)
        .uncaught(() => { throw new Error('1 has been uncught'); }); // won't throw uncaught
    

    In such a handler, the whole Promise chain should act depend on what it returns:

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

    If you want a default handler does not change the state of a Promise, we can define another two methods like Promise.prototype.onFail or Promise.prototype.onSuccess to support for default successful or failed handling:

    Promise.reject(1).onFail(console.log); // => 1
    Promise.reject(2).then(ignore, () => Promise.resolve(1)).onSuccess(console.log); // => 2 
    

Can you maybe give an example of how you envision this uncaught method to be used? It's not quite clear what you are after.

However, it sounds like you are looking to find whether a specific promise got rejected and the error did not get handled anywhere else. This is not possible. Handlers do not "report back" to the promise whether an error got handled or not. Any .catch(), .then() (and by extension await) call simply forwards errors that didn't get handled and will reject another promise with it. The internal state of the original promise is marked as "handled". The unhandeldRejection event only fires for the promises that are the end of a chain.

@bergus Two situations:

  1. If we want to check whether a Promise chain has uncaught rejected result in the middle of processes, we can use Promise.prototype.uncaught:

    const handler = () => console.log('uncaught'), caught = r => r > 1, ignore = () => {};
    Promise.reject(1).uncaught(handler); // => "uncaught"
    Promise.reject(1).uncaught(handler).catch(caught); // => "uncaught"
    Promise.reject(1).catch(caught).uncaught(handler); // => won't fired as already been caught
    Promise.reject(1).then(ignore, caught).uncuaght(handler); // => won't fired as already been caught
    
  2. If we want to check finally, wen can use Promise.uncaught:

    const handler = () => console.log('unhandled'), caught = r => r > 1, ignore = () => {};
    Promise.uncaught(Promise.reject(1), handler); // => "unhandled"
    Promise.uncaught(Promise.reject(1), handler).catch(caught); // => won't fired as already been caught
    Promise.uncaught(Promise.reject(1), handler).then(ignore, caught); // => won't fired as already been caught
    

node does indeed fire the event on Promise.reject() by itself. I’m not sure i understand your second example.

@ljharb It it as same as Promise.reject(1) to some extent. To make it simple

Promise.reject(1); // fire
Promise.reject(1).catch(r => r > 1); // not fired
Promise.reject(1).then(() => {}, r => r > 1); // not fired

Sure, in both of those “not fired” cases, the promise is caught/handled. The two catch handlers never throw in those examples, so those promises can never be uncaught, which is why it’s not fired.

Exactly. So what can I do before making it as a official proposal?

My understanding of the use case of the global uncaught rejection handlers is for promises you forgot to handle. If you remembered to do .uncaught, why not just do .catch?

1 Like

For instance, we want a default handler to handle any other exceptions we may not know, especially when integrating Promise with XMLHttpRequest:

const catchCode = (expected, cb) =>
    code => code !== expected ? Promise.reject(code) : cb();

Promise.reject(anyCode)
    .catch(catchCode(1, () => console.log('catch 1')))
    .catch(catchCode(2, () => console.log('catch 2')))
    .uncaught(() => console.log('default handler for any other code'));

Besides, it is better for code reading than:

Promise.reject(anyCode)
    .catch(catchCode(1, () => console.log('catch 1')))
    .catch(catchCode(2, () => console.log('catch 2')))
    .catch(() => console.log('default handler for any other code'));

That seems like “just .catch without using your catchCode abstraction”?

2 Likes

In my opinion, catch is for exact exceptions, while uncaught for unknown or unhandled exceptions. And so far as you said, we just only need a global function for promises we forgot to handle.

Promise.uncaught(
    Promise.reject(anyCode),
    () => console.log('default handler for any other code')
)
    .catch(catchCode(1, () => console.log('catch 1')))
    .catch(catchCode(2, () => console.log('catch 2')))    

currently catch is for any rejection (rejections aren’t necessarily exceptions), including unknown ones.

1 Like

In your first situation, it seems using standard .catch() would have the exact same result as you desire.

For your second situation, you can write your own Promise.uncaught, but it is pretty hacky and will never work in the middle of a chain, especially not with uncaught(somePromise, handler).then(ignore).

So is it necessary to make it as a proposal to add such a method?

It would be necessary, but I don't see why it would be beneficial to add such a method, since .catch already serves this purpose. In other words, while it's perfectly valid for you to choose to see catch as "for exact exceptions", that's not how it's defined nor how it's commonly used.

1 Like

Like what @bergus said above, standard Promise.prototype.catch can handle the first situation, except detecting unhandled Promise like https://stackoverflow.com/a/57792542.

1 Like

I have created a draft proposal, and feel free to discuss it.

If I want to register a global default error handler for any promises, I need to override the method Promise.prototype.catch? In such a situation, if I don't want to handle it in one catching block, I need to reject it again to give to default handler?

const fn = Promise.prototype.catch;
Object.assign(Promise.prototype, {
    catch() {
        if (this instanceof UnhandledPromise) {
            // default handling
        } else {
            return fn.apply(this, arguments);
        }
    }
});

doSomething().catch(err => {
    if (err.code === 1) {
        // handler error code 1 here 
    } else {
        // give to a default handler
        return Promise.reject(new UnhandledPromise(err));
    }
})