Skip to content

Commit 66484b8

Browse files
BridgeARtargos
authored andcommitted
process: add multipleResolves event
This adds the `multipleResolves` event to track promises that resolve more than once or that reject after resolving. It is important to expose this to the user to make sure the application runs as expected. Without such warnings it would be very hard to debug these situations. PR-URL: #22218 Fixes: nodejs/promises-debugging#8 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Franziska Hinkelmann <[email protected]> Reviewed-By: Anatoli Papirovski <[email protected]>
1 parent f66e9ab commit 66484b8

File tree

6 files changed

+177
-35
lines changed

6 files changed

+177
-35
lines changed

doc/api/process.md

+53
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,59 @@ the child process.
9797
The message goes through serialization and parsing. The resulting message might
9898
not be the same as what is originally sent.
9999

100+
### Event: 'multipleResolves'
101+
<!-- YAML
102+
added: REPLACEME
103+
-->
104+
105+
* `type` {string} The error type. One of `'resolve'` or `'reject'`.
106+
* `promise` {Promise} The promise that resolved or rejected more than once.
107+
* `value` {any} The value with which the promise was either resolved or
108+
rejected after the original resolve.
109+
110+
The `'multipleResolves'` event is emitted whenever a `Promise` has been either:
111+
112+
* Resolved more than once.
113+
* Rejected more than once.
114+
* Rejected after resolve.
115+
* Resolved after reject.
116+
117+
This is useful for tracking errors in your application while using the promise
118+
constructor. Otherwise such mistakes are silently swallowed due to being in a
119+
dead zone.
120+
121+
It is recommended to end the process on such errors, since the process could be
122+
in an undefined state. While using the promise constructor make sure that it is
123+
guaranteed to trigger the `resolve()` or `reject()` functions exactly once per
124+
call and never call both functions in the same call.
125+
126+
```js
127+
process.on('multipleResolves', (type, promise, reason) => {
128+
console.error(type, promise, reason);
129+
setImmediate(() => process.exit(1));
130+
});
131+
132+
async function main() {
133+
try {
134+
return await new Promise((resolve, reject) => {
135+
resolve('First call');
136+
resolve('Swallowed resolve');
137+
reject(new Error('Swallowed reject'));
138+
});
139+
} catch {
140+
throw new Error('Failed');
141+
}
142+
}
143+
144+
main().then(console.log);
145+
// resolve: Promise { 'First call' } 'Swallowed resolve'
146+
// reject: Promise { 'First call' } Error: Swallowed reject
147+
// at Promise (*)
148+
// at new Promise (<anonymous>)
149+
// at main (*)
150+
// First call
151+
```
152+
100153
### Event: 'rejectionHandled'
101154
<!-- YAML
102155
added: v1.4.1

lib/internal/process/promises.js

+28-14
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,37 @@ const { safeToString } = process.binding('util');
55
const maybeUnhandledPromises = new WeakMap();
66
const pendingUnhandledRejections = [];
77
const asyncHandledRejections = [];
8+
const promiseRejectEvents = {};
89
let lastPromiseId = 0;
910

1011
exports.setup = setupPromises;
1112

1213
function setupPromises(_setupPromises) {
13-
_setupPromises(unhandledRejection, handledRejection);
14+
_setupPromises(handler, promiseRejectEvents);
1415
return emitPromiseRejectionWarnings;
1516
}
1617

18+
function handler(type, promise, reason) {
19+
switch (type) {
20+
case promiseRejectEvents.kPromiseRejectWithNoHandler:
21+
return unhandledRejection(promise, reason);
22+
case promiseRejectEvents.kPromiseHandlerAddedAfterReject:
23+
return handledRejection(promise);
24+
case promiseRejectEvents.kPromiseResolveAfterResolved:
25+
return resolveError('resolve', promise, reason);
26+
case promiseRejectEvents.kPromiseRejectAfterResolved:
27+
return resolveError('reject', promise, reason);
28+
}
29+
}
30+
31+
function resolveError(type, promise, reason) {
32+
// We have to wrap this in a next tick. Otherwise the error could be caught by
33+
// the executed promise.
34+
process.nextTick(() => {
35+
process.emit('multipleResolves', type, promise, reason);
36+
});
37+
}
38+
1739
function unhandledRejection(promise, reason) {
1840
maybeUnhandledPromises.set(promise, {
1941
reason,
@@ -45,16 +67,6 @@ function handledRejection(promise) {
4567

4668
const unhandledRejectionErrName = 'UnhandledPromiseRejectionWarning';
4769
function emitWarning(uid, reason) {
48-
try {
49-
if (reason instanceof Error) {
50-
process.emitWarning(reason.stack, unhandledRejectionErrName);
51-
} else {
52-
process.emitWarning(safeToString(reason), unhandledRejectionErrName);
53-
}
54-
} catch (e) {
55-
// ignored
56-
}
57-
5870
// eslint-disable-next-line no-restricted-syntax
5971
const warning = new Error(
6072
'Unhandled promise rejection. This error originated either by ' +
@@ -66,10 +78,12 @@ function emitWarning(uid, reason) {
6678
try {
6779
if (reason instanceof Error) {
6880
warning.stack = reason.stack;
81+
process.emitWarning(reason.stack, unhandledRejectionErrName);
82+
} else {
83+
process.emitWarning(safeToString(reason), unhandledRejectionErrName);
6984
}
70-
} catch (err) {
71-
// ignored
72-
}
85+
} catch {}
86+
7387
process.emitWarning(warning);
7488
emitDeprecationWarning();
7589
}

src/bootstrapper.cc

+36-19
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@ using v8::Context;
1212
using v8::Function;
1313
using v8::FunctionCallbackInfo;
1414
using v8::Isolate;
15+
using v8::kPromiseHandlerAddedAfterReject;
16+
using v8::kPromiseRejectAfterResolved;
17+
using v8::kPromiseRejectWithNoHandler;
18+
using v8::kPromiseResolveAfterResolved;
1519
using v8::Local;
1620
using v8::MaybeLocal;
21+
using v8::Number;
1722
using v8::Object;
1823
using v8::Promise;
1924
using v8::PromiseRejectEvent;
@@ -61,34 +66,40 @@ void PromiseRejectCallback(PromiseRejectMessage message) {
6166
PromiseRejectEvent event = message.GetEvent();
6267

6368
Environment* env = Environment::GetCurrent(isolate);
69+
6470
if (env == nullptr) return;
65-
Local<Function> callback;
71+
72+
Local<Function> callback = env->promise_handler_function();
6673
Local<Value> value;
74+
Local<Value> type = Number::New(env->isolate(), event);
6775

68-
if (event == v8::kPromiseRejectWithNoHandler) {
69-
callback = env->promise_reject_unhandled_function();
76+
if (event == kPromiseRejectWithNoHandler) {
7077
value = message.GetValue();
71-
72-
if (value.IsEmpty())
73-
value = Undefined(isolate);
74-
7578
unhandledRejections++;
76-
} else if (event == v8::kPromiseHandlerAddedAfterReject) {
77-
callback = env->promise_reject_handled_function();
79+
TRACE_COUNTER2(TRACING_CATEGORY_NODE2(promises, rejections),
80+
"rejections",
81+
"unhandled", unhandledRejections,
82+
"handledAfter", rejectionsHandledAfter);
83+
} else if (event == kPromiseHandlerAddedAfterReject) {
7884
value = Undefined(isolate);
79-
8085
rejectionsHandledAfter++;
86+
TRACE_COUNTER2(TRACING_CATEGORY_NODE2(promises, rejections),
87+
"rejections",
88+
"unhandled", unhandledRejections,
89+
"handledAfter", rejectionsHandledAfter);
90+
} else if (event == kPromiseResolveAfterResolved) {
91+
value = message.GetValue();
92+
} else if (event == kPromiseRejectAfterResolved) {
93+
value = message.GetValue();
8194
} else {
8295
return;
8396
}
8497

85-
TRACE_COUNTER2(TRACING_CATEGORY_NODE2(promises, rejections),
86-
"rejections",
87-
"unhandled", unhandledRejections,
88-
"handledAfter", rejectionsHandledAfter);
89-
98+
if (value.IsEmpty()) {
99+
value = Undefined(isolate);
100+
}
90101

91-
Local<Value> args[] = { promise, value };
102+
Local<Value> args[] = { type, promise, value };
92103
MaybeLocal<Value> ret = callback->Call(env->context(),
93104
Undefined(isolate),
94105
arraysize(args),
@@ -103,11 +114,17 @@ void SetupPromises(const FunctionCallbackInfo<Value>& args) {
103114
Isolate* isolate = env->isolate();
104115

105116
CHECK(args[0]->IsFunction());
106-
CHECK(args[1]->IsFunction());
117+
CHECK(args[1]->IsObject());
118+
119+
Local<Object> constants = args[1].As<Object>();
120+
121+
NODE_DEFINE_CONSTANT(constants, kPromiseRejectWithNoHandler);
122+
NODE_DEFINE_CONSTANT(constants, kPromiseHandlerAddedAfterReject);
123+
NODE_DEFINE_CONSTANT(constants, kPromiseResolveAfterResolved);
124+
NODE_DEFINE_CONSTANT(constants, kPromiseRejectAfterResolved);
107125

108126
isolate->SetPromiseRejectCallback(PromiseRejectCallback);
109-
env->set_promise_reject_unhandled_function(args[0].As<Function>());
110-
env->set_promise_reject_handled_function(args[1].As<Function>());
127+
env->set_promise_handler_function(args[0].As<Function>());
111128
}
112129

113130
#define BOOTSTRAP_METHOD(name, fn) env->SetMethod(bootstrapper, #name, fn)

src/env.h

+1-2
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,7 @@ struct PackageConfig {
342342
V(performance_entry_callback, v8::Function) \
343343
V(performance_entry_template, v8::Function) \
344344
V(process_object, v8::Object) \
345-
V(promise_reject_handled_function, v8::Function) \
346-
V(promise_reject_unhandled_function, v8::Function) \
345+
V(promise_handler_function, v8::Function) \
347346
V(promise_wrap_template, v8::ObjectTemplate) \
348347
V(push_values_to_array_function, v8::Function) \
349348
V(sab_lifetimepartner_constructor_template, v8::FunctionTemplate) \

test/message/unhandled_promise_trace_warnings.out

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
at *
3535
(node:*) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
3636
at handledRejection (internal/process/promises.js:*)
37+
at handler (internal/process/promises.js:*)
3738
at Promise.then *
3839
at Promise.catch *
3940
at Immediate.setImmediate (*test*message*unhandled_promise_trace_warnings.js:*)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
6+
const rejection = new Error('Swallowed reject');
7+
const rejection2 = new TypeError('Weird');
8+
const resolveMessage = 'First call';
9+
const rejectPromise = new Promise((r) => setTimeout(r, 10, rejection2));
10+
const swallowedResolve = 'Swallowed resolve';
11+
const swallowedResolve2 = 'Foobar';
12+
13+
process.on('multipleResolves', common.mustCall(handler, 4));
14+
15+
const p1 = new Promise((resolve, reject) => {
16+
resolve(resolveMessage);
17+
resolve(swallowedResolve);
18+
reject(rejection);
19+
});
20+
21+
const p2 = new Promise((resolve, reject) => {
22+
reject(rejectPromise);
23+
resolve(swallowedResolve2);
24+
reject(rejection2);
25+
}).catch(common.mustCall((exception) => {
26+
assert.strictEqual(exception, rejectPromise);
27+
}));
28+
29+
const expected = [
30+
'resolve',
31+
p1,
32+
swallowedResolve,
33+
'reject',
34+
p1,
35+
rejection,
36+
'resolve',
37+
p2,
38+
swallowedResolve2,
39+
'reject',
40+
p2,
41+
rejection2
42+
];
43+
44+
let count = 0;
45+
46+
function handler(type, promise, reason) {
47+
assert.strictEqual(type, expected.shift());
48+
// In the first two cases the promise is identical because it's not delayed.
49+
// The other two cases are not identical, because the `promise` is caught in a
50+
// state when it has no knowledge about the `.catch()` handler that is
51+
// attached to it right afterwards.
52+
if (count++ < 2) {
53+
assert.strictEqual(promise, expected.shift());
54+
} else {
55+
assert.notStrictEqual(promise, expected.shift());
56+
}
57+
assert.strictEqual(reason, expected.shift());
58+
}

0 commit comments

Comments
 (0)