First occurrence
The first time I saw this warning today:
1 | if(!Promise.first){ |
Console log with node --trace-warnings flag:
1 | > (node:94838) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 3) |
If instead of catch, I pass two callbacks to then, the warning still shows like before:
1 | if(!Promise.first){ |
But if I run Promise.first again, the warning does not appear any more.
Minimum reproduction
I reduced the reproduction to this:
1 | const k = Promise.reject(3) |
Or:
1 | const k = Promise.reject(3) |
1 | const k = Promise.reject(3) |
It is also notable that if I call k.then() again afterwards, I get no warning; and if I just do const k = Promise.reject(3) I don’t get the warning either.
By contrast, this does not fire the warning:
1 | Promise.reject(3).then() |
This does not fire the warning either:
1 | (() => { |
So there are two conditions to fire this warning:
- Promise defined on root level, i.e. assigned to a global object.
- No rejection handler is registered to it at the time of promise creation. The
processPromiseRejectionsflags promise with no handlers synchronously so a statement right after the declaration still does not prevent the warning from happening.
Investigation
My search takes me to here:
https://github.com/nodejs/node/blob/main/lib/internal/process/promises.js#L166
When the Promise is defined on the root, is rejected but not handled (not passed a then or catch or finally phrase), it is of type kPromiseRejectWithNoHandler and gets added to a weak map maybeUnhandledPromises with some info, and also pushed to array pendingUnhandledRejections. processPromiseRejections will synchronously mark warned field of this promise to be true.
But later I chain to this promise a then or catch or finally that attempts to handle the rejection, the promiseRejectHandler will try to handle the rejection because the rejection type is now kPromiseHandlerAddedAfterReject. handledRejection gets called. handleRejection finds out that this promise is still in maybeUnhandledPromises with warned set to true, therefore it fires a PromiseRejectionHandledWarning: Promise rejection was handled asynchronously
This only warns once because once this rejection is handled, it gets removed from maybeUnhandledPromises by handledRejection:
1 | maybeUnhandledPromises.delete(promise); |
Back to the original warning
So if I do not put p into a variable and instead pass a Promise.reject(3) to Promise.first I don’t get the warning:
1 | if(!Promise.first){ |