First occurrence

The first time I saw this warning today:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if(!Promise.first){
Promise.first = function(prs) {
return new Promise((resolve, reject) => {
let rejectCount = 0
prs.forEach(pr => {
Promise.resolve(pr)
.then(function onFulfilled(v){resolve(v)})
.catch(
function onRejected(){
rejectCount++
if(rejectCount === prs.length) {
reject('All promises rejected!')
}
})
})
})
}
}

const p = Promise.reject(4)
Promise.first([p])

Console log with node --trace-warnings flag:

1
2
3
4
5
6
7
8
9
10
11
> (node:94838) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 3)
at handledRejection (node:internal/process/promises:163:23)
at promiseRejectHandler (node:internal/process/promises:116:7)
at Promise.then (<anonymous>)
at REPL18:7:14
at Array.forEach (<anonymous>)
at REPL18:5:11
at new Promise (<anonymous>)
at Function.Promise.first (REPL18:3:12)
at REPL24:1:9
at Script.runInThisContext (node:vm:129:12)

If instead of catch, I pass two callbacks to then, the warning still shows like before:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if(!Promise.first){
Promise.first = function(prs) {
return new Promise((resolve, reject) => {
let rejectCount = 0
prs.forEach(pr => {
Promise.resolve(pr).then(
function onFulfilled(v){resolve(v)},
function onRejected(){
rejectCount++
if(rejectCount === prs.length) {
reject('All promises rejected!')
}
})
})
})
}
}
const p = Promise.reject(4)
const r = Promise.first([p])

But if I run Promise.first again, the warning does not appear any more.

Minimum reproduction

I reduced the reproduction to this:

1
2
const k = Promise.reject(3)
k.then()

Or:

1
2
const k = Promise.reject(3)
k.catch()
1
2
const k = Promise.reject(3)
k.finally()

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
2
3
4
(() => {
const k = Promise.reject(3)
k.then()
})()

So there are two conditions to fire this warning:

  1. Promise defined on root level, i.e. assigned to a global object.
  2. No rejection handler is registered to it at the time of promise creation. The processPromiseRejections flags 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if(!Promise.first){
Promise.first = function(prs) {
return new Promise((resolve, reject) => {
let rejectCount = 0
prs.forEach(pr => {
Promise.resolve(pr).then(
function onFulfilled(v){resolve(v)},
function onRejected(){
rejectCount++
if(rejectCount === prs.length) {
reject('All promises rejected!')
}
})
})
})
}
}

const r = Promise.first([Promise.reject(4)])