24

I have just installed Node v7.2.0 and learned that the following code:

var prm = Promise.reject(new Error('fail'));

results in this message:;

(node:4786) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: fail
(node:4786) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

I understand the reasoning behind this as many programmers have probably experienced the frustration of an Error ending up being swallowed by a Promise. However then I did this experiment:

var prm = Promise.reject(new Error('fail'));

setTimeout(() => {
    prm.catch((err) => {
        console.log(err.message);
    })
},
0)

which results in:

(node:4860) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: fail
(node:4860) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
(node:4860) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
fail

I on basis of the PromiseRejectionHandledWarning assume that handling a Promise rejection asynchronously is/might be a bad thing.

But why is that?

rabbitco
  • 2,790
  • 3
  • 16
  • 38
  • To me that just looks like a failure to detect the right conditions for the warning because you do have a `.catch()` handler and thus the reject is handled. But, adding a `.catch()` in a `setTimeout()` seems screwy and with no legitimate purpose. Better to do `prm.catch(err => setTimeout(...))` and avoid the whole issue. There is no reason to add the `.catch()` handler later. – jfriend00 Dec 01 '16 at 21:24
  • 1
    @jfriend00: I agree the example above has little practical use. It is intentionally made very short to provide the minimum amount of code to illustrate the question. However I have a "real life" appliance for async rejection handling ... – rabbitco Dec 01 '16 at 21:35
  • You'd have to show us what you think is a "real life" situation where you want to attach the only `.catch()` handler some time in the future. I don't see it and you can certainly see why that's impossible for the underlying engine to know you're going to do that because until you do, it's a promise with no reject handler. – jfriend00 Dec 01 '16 at 21:42

2 Answers2

35

Note: See 2020 updates below for changes in Node v15

"Should I refrain from handling Promise rejection asynchronously?"

Those warnings serve an important purpose but to see how it all works see those examples:

Try this:

process.on('unhandledRejection', () => {});
process.on('rejectionHandled', () => {});

var prm = Promise.reject(new Error('fail'));

setTimeout(() => {
    prm.catch((err) => {
        console.log(err.message);
    })
}, 0);

Or this:

var prm = Promise.reject(new Error('fail'));
prm.catch(() => {});

setTimeout(() => {
    prm.catch((err) => {
        console.log(err.message);
    })
}, 0);

Or this:

var caught = require('caught');
var prm = caught(Promise.reject(new Error('fail')));

setTimeout(() => {
    prm.catch((err) => {
        console.log(err.message);
    })
}, 0);

Disclaimer: I am the author of the caught module (and yes, I wrote it for this answer).

Rationale

It was added to Node as one of the Breaking changes between v6 and v7. There was a heated discussion about it in Issue #830: Default Unhandled Rejection Detection Behavior with no universal agreement on how promises with rejection handlers attached asynchronously should behave - work without warnings, work with warnings or be forbidden to use at all by terminating the program. More discussion took place in several issues of the unhandled-rejections-spec project.

This warning is to help you find situations where you forgot to handle the rejection but sometimes you may want to avoid it. For example you may want to make a bunch of requests and store the resulting promises in an array, only to handle it later in some other part of your program.

One of the advantages of promises over callbacks is that you can separate the place where you create the promise from the place (or places) where you attach the handlers. Those warnings make it more difficult to do but you can either handle the events (my first example) or attach a dummy catch handler wherever you create a promise that you don't want to handle right away (second example). Or you can have a module do it for you (third example).

Avoiding warnings

Attaching an empty handler doesn't change the way how the stored promise works in any way if you do it in two steps:

var prm1 = Promise.reject(new Error('fail'));
prm1.catch(() => {});

This will not be the same, though:

var prm2 = Promise.reject(new Error('fail')).catch(() => {});

Here prm2 will be a different promise then prm1. While prm1 will be rejected with 'fail' error, prm2 will be resolved with undefined which is probably not what you want.

But you could write a simple function to make it work like a two-step example above, like I did with the caught module:

var prm3 = caught(Promise.reject(new Error('fail')));

Here prm3 is the same as prm1.

See: https://www.npmjs.com/package/caught

2017 Update

See also Pull Request #6375: lib,src: "throw" on unhandled promise rejections (not merged yet as of Febryary 2017) that is marked as Milestone 8.0.0:

Makes Promises "throw" rejections which exit like regular uncaught errors. [emphasis added]

This means that we can expect Node 8.x to change the warning that this question is about into an error that crashes and terminates the process and we should take it into account while writing our programs today to avoid surprises in the future.

See also the Node.js 8.0.0 Tracking Issue #10117.

2020 Update

See also Pull Request #33021: process: Change default --unhandled-rejections=throw (already merged and released as part of the v15 release - see: release notes) that once again makes it an exception:

As of Node.js 15, the default mode for unhandledRejection is changed to throw (from warn). In throw mode, if an unhandledRejection hook is not set, the unhandledRejection is raised as an uncaught exception. Users that have an unhandledRejection hook should see no change in behavior, and it’s still possible to switch modes using the --unhandled-rejections=mode process flag.

This means that Node 15.x has finally changed the warning that this question is about into an error so as I said in 2017 above, we should definitely take it into account while writing our programs because if we don't then it will definitely cause problems when upgrading the runtime to Node 15.x or higher.

advaith
  • 3
  • 1
  • 4
rsp
  • 107,747
  • 29
  • 201
  • 177
  • 1
    I don't think node 8 crashes yet, maybe node 9. The process.on('unhandledRejection', () => {}); and process.on('rejectionHandled', () => {}); should really be advocated more. Thanks for your detailed explanation! – nicojs Aug 04 '17 at 21:00
  • 2
    To ensure full accuracy, I think it's important to clarify the exact change which will be coming (no earlier than Node 10, as 9 has already shipped without). It will only exit with a non-zero code if the Promise is garbage collected. So only when you have no reference to the Promise and thus it's impossible for it to ever be caught. Asynchronously caught Promises will never cause the process to exit, as this would break a number of standard patterns. The warnings do still come up for these, as there's no way to know whether you will catch it. – James Billingham Jan 11 '18 at 01:59
  • @JamesBillingham is what you clarified in your comment still the case in the current behavior of Node v15 as released few days ago? (more info in the update to the answer) – rsp Oct 27 '20 at 12:35
2

I assume that handling a Promise rejection asynchronously is a bad thing.

Yes indeed it is.

It is expected that you want to handle any rejections immediately anyway. If you fail to do (and possibly fail to ever handle it), you'll get a warning.
I've experienced hardly any situations where you wouldn't want to fail immediately after getting a rejection. And even if you need to wait for something further after the fail, you should do that explicitly.

I've never seen a case where it would be impossible to immediately install the error handler (try to convince me otherwise). In your case, if you want a slightly delayed error message, just do

var prm = Promise.reject(new Error('fail'));

prm.catch((err) => {
    setTimeout(() => {
        console.log(err.message);
    }, 0);
});
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I have an issue where I get `UnhandledPromiseRejectionWarning` inside Webstorm even though I actually *handle* the rejected `Promise` inside a `Generator`. When run directly from the console I do not receive a warning. So even though you are probably totally right your answer did not help me - not your fault though as you just answered my question. It was deliberately kept simple even though there were more layers to it. – rabbitco Dec 02 '16 at 11:37
  • If you can post your actual code in a new question, I could help you with it :-) – Bergi Dec 02 '16 at 15:07
  • 1
    That is very kind of you to offer. I have now prepared a [new question](http://stackoverflow.com/questions/40963671/receiving-unhandledpromiserejectionwarning-even-though-rejected-promise-is-han) with the entire code that produces the problem. Prepare for a "wall of code". – rabbitco Dec 04 '16 at 21:10