Skip to content

Commit 220a600

Browse files
mcollinatargos
authored andcommitted
events: add captureRejection option
PR-URL: #27867 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Jeremiah Senkpiel <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Michaël Zasso <[email protected]>
1 parent 355b48b commit 220a600

File tree

3 files changed

+528
-6
lines changed

3 files changed

+528
-6
lines changed

doc/api/events.md

+125
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,66 @@ myEmitter.emit('error', new Error('whoops!'));
155155
// Prints: whoops! there was an error
156156
```
157157

158+
## Capture Rejections of Promises
159+
160+
> Stability: 1 - captureRejections is experimental.
161+
162+
Using `async` functions with event handlers is problematic, because it
163+
can lead to an unhandled rejection in case of a thrown exception:
164+
165+
```js
166+
const ee = new EventEmitter();
167+
ee.on('something', async (value) => {
168+
throw new Error('kaboom');
169+
});
170+
```
171+
172+
The `captureRejections` option in the `EventEmitter` constructor or the global
173+
setting change this behavior, installing a `.then(undefined, handler)`
174+
handler on the `Promise`. This handler routes the exception
175+
asynchronously to the [`Symbol.for('nodejs.rejection')`][rejection] method
176+
if there is one, or to [`'error'`][error] event handler if there is none.
177+
178+
```js
179+
const ee1 = new EventEmitter({ captureRejections: true });
180+
ee1.on('something', async (value) => {
181+
throw new Error('kaboom');
182+
});
183+
184+
ee1.on('error', console.log);
185+
186+
const ee2 = new EventEmitter({ captureRejections: true });
187+
ee2.on('something', async (value) => {
188+
throw new Error('kaboom');
189+
});
190+
191+
ee2[Symbol.for('nodejs.rejection')] = console.log;
192+
```
193+
194+
Setting `EventEmitter.captureRejections = true` will change the default for all
195+
new instances of `EventEmitter`.
196+
197+
```js
198+
EventEmitter.captureRejections = true;
199+
const ee1 = new EventEmitter();
200+
ee1.on('something', async (value) => {
201+
throw new Error('kaboom');
202+
});
203+
204+
ee1.on('error', console.log);
205+
```
206+
207+
The `'error'` events that are generated by the `captureRejections` behavior
208+
do not have a catch handler to avoid infinite error loops: the
209+
recommendation is to **not use `async` functions as `'error'` event handlers**.
210+
158211
## Class: EventEmitter
159212
<!-- YAML
160213
added: v0.1.26
214+
changes:
215+
- version: REPLACEME
216+
pr-url: https://github.com/nodejs/node/pull/27867
217+
description: Added captureRejections option.
161218
-->
162219

163220
The `EventEmitter` class is defined and exposed by the `events` module:
@@ -169,6 +226,12 @@ const EventEmitter = require('events');
169226
All `EventEmitter`s emit the event `'newListener'` when new listeners are
170227
added and `'removeListener'` when existing listeners are removed.
171228

229+
It supports the following option:
230+
231+
* `captureRejections` {boolean} It enables
232+
[automatic capturing of promise rejection][capturerejections].
233+
Default: `false`.
234+
172235
### Event: 'newListener'
173236
<!-- YAML
174237
added: v0.1.26
@@ -694,6 +757,42 @@ newListeners[0]();
694757
emitter.emit('log');
695758
```
696759

760+
### emitter\[Symbol.for('nodejs.rejection')\](err, eventName\[, ...args\])
761+
<!-- YAML
762+
added: REPLACEME
763+
-->
764+
765+
> Stability: 1 - captureRejections is experimental.
766+
767+
* `err` Error
768+
* `eventName` {string|symbol}
769+
* `...args` {any}
770+
771+
The `Symbol.for('nodejs.rejection')` method is called in case a
772+
promise rejection happens when emitting an event and
773+
[`captureRejections`][capturerejections] is enabled on the emitter.
774+
It is possible to use [`events.captureRejectionSymbol`][rejectionsymbol] in
775+
place of `Symbol.for('nodejs.rejection')`.
776+
777+
```js
778+
const { EventEmitter, captureRejectionSymbol } = require('events');
779+
780+
class MyClass extends EventEmitter {
781+
constructor() {
782+
super({ captureRejections: true });
783+
}
784+
785+
[captureRejectionSymbol](err, event, ...args) {
786+
console.log('rejection happened for', event, 'with', err, ...args);
787+
this.destroy(err);
788+
}
789+
790+
destroy(err) {
791+
// Tear the resource down here.
792+
}
793+
}
794+
```
795+
697796
## events.once(emitter, name)
698797
<!-- YAML
699798
added: v11.13.0
@@ -740,6 +839,28 @@ async function run() {
740839
run();
741840
```
742841

842+
## events.captureRejections
843+
<!-- YAML
844+
added: REPLACEME
845+
-->
846+
847+
> Stability: 1 - captureRejections is experimental.
848+
849+
Value: {boolean}
850+
851+
Change the default `captureRejections` option on all new `EventEmitter` objects.
852+
853+
## events.captureRejectionSymbol
854+
<!-- YAML
855+
added: REPLACEME
856+
-->
857+
858+
> Stability: 1 - captureRejections is experimental.
859+
860+
Value: `Symbol.for('nodejs.rejection')`
861+
862+
See how to write a custom [rejection handler][rejection].
863+
743864
[WHATWG-EventTarget]: https://dom.spec.whatwg.org/#interface-eventtarget
744865
[`--trace-warnings`]: cli.html#cli_trace_warnings
745866
[`EventEmitter.defaultMaxListeners`]: #events_eventemitter_defaultmaxlisteners
@@ -751,3 +872,7 @@ run();
751872
[`net.Server`]: net.html#net_class_net_server
752873
[`process.on('warning')`]: process.html#process_event_warning
753874
[stream]: stream.html
875+
[capturerejections]: #events_capture_rejections_of_promises
876+
[rejection]: #events_emitter_symbol_for_nodejs_rejection_err_eventname_args
877+
[rejectionsymbol]: #events_events_capturerejectionsymbol
878+
[error]: #events_error_events

lib/events.js

+106-6
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
const {
2525
Array,
26+
Boolean,
2627
MathMin,
2728
NumberIsNaN,
2829
ObjectCreate,
@@ -32,6 +33,7 @@ const {
3233
ReflectApply,
3334
ReflectOwnKeys,
3435
} = primordials;
36+
const kRejection = Symbol.for('nodejs.rejection');
3537

3638
let spliceOne;
3739

@@ -49,8 +51,10 @@ const {
4951
inspect
5052
} = require('internal/util/inspect');
5153

52-
function EventEmitter() {
53-
EventEmitter.init.call(this);
54+
const kCapture = Symbol('kCapture');
55+
56+
function EventEmitter(opts) {
57+
EventEmitter.init.call(this, opts);
5458
}
5559
module.exports = EventEmitter;
5660
module.exports.once = once;
@@ -60,6 +64,29 @@ EventEmitter.EventEmitter = EventEmitter;
6064

6165
EventEmitter.usingDomains = false;
6266

67+
EventEmitter.captureRejectionSymbol = kRejection;
68+
ObjectDefineProperty(EventEmitter, 'captureRejections', {
69+
get() {
70+
return EventEmitter.prototype[kCapture];
71+
},
72+
set(value) {
73+
if (typeof value !== 'boolean') {
74+
throw new ERR_INVALID_ARG_TYPE('EventEmitter.captureRejections',
75+
'boolean', value);
76+
}
77+
78+
EventEmitter.prototype[kCapture] = value;
79+
},
80+
enumerable: true
81+
});
82+
83+
// The default for captureRejections is false
84+
ObjectDefineProperty(EventEmitter.prototype, kCapture, {
85+
value: false,
86+
writable: true,
87+
enumerable: false
88+
});
89+
6390
EventEmitter.prototype._events = undefined;
6491
EventEmitter.prototype._eventsCount = 0;
6592
EventEmitter.prototype._maxListeners = undefined;
@@ -89,7 +116,7 @@ ObjectDefineProperty(EventEmitter, 'defaultMaxListeners', {
89116
}
90117
});
91118

92-
EventEmitter.init = function() {
119+
EventEmitter.init = function(opts) {
93120

94121
if (this._events === undefined ||
95122
this._events === ObjectGetPrototypeOf(this)._events) {
@@ -98,8 +125,64 @@ EventEmitter.init = function() {
98125
}
99126

100127
this._maxListeners = this._maxListeners || undefined;
128+
129+
130+
if (opts && opts.captureRejections) {
131+
if (typeof opts.captureRejections !== 'boolean') {
132+
throw new ERR_INVALID_ARG_TYPE('options.captureRejections',
133+
'boolean', opts.captureRejections);
134+
}
135+
this[kCapture] = Boolean(opts.captureRejections);
136+
} else {
137+
// Assigning it directly a prototype lookup, as it slighly expensive
138+
// and it sits in a very sensitive hot path.
139+
this[kCapture] = EventEmitter.prototype[kCapture];
140+
}
101141
};
102142

143+
function addCatch(that, promise, type, args) {
144+
if (!that[kCapture]) {
145+
return;
146+
}
147+
148+
// Handle Promises/A+ spec, then could be a getter
149+
// that throws on second use.
150+
try {
151+
const then = promise.then;
152+
153+
if (typeof then === 'function') {
154+
then.call(promise, undefined, function(err) {
155+
// The callback is called with nextTick to avoid a follow-up
156+
// rejection from this promise.
157+
process.nextTick(emitUnhandledRejectionOrErr, that, err, type, args);
158+
});
159+
}
160+
} catch (err) {
161+
that.emit('error', err);
162+
}
163+
}
164+
165+
function emitUnhandledRejectionOrErr(ee, err, type, args) {
166+
if (typeof ee[kRejection] === 'function') {
167+
ee[kRejection](err, type, ...args);
168+
} else {
169+
// We have to disable the capture rejections mechanism, otherwise
170+
// we might end up in an infinite loop.
171+
const prev = ee[kCapture];
172+
173+
// If the error handler throws, it is not catcheable and it
174+
// will end up in 'uncaughtException'. We restore the previous
175+
// value of kCapture in case the uncaughtException is present
176+
// and the exception is handled.
177+
try {
178+
ee[kCapture] = false;
179+
ee.emit('error', err);
180+
} finally {
181+
ee[kCapture] = prev;
182+
}
183+
}
184+
}
185+
103186
// Obviously not all Emitters should be limited to 10. This function allows
104187
// that to be increased. Set to zero for unlimited.
105188
EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
@@ -216,12 +299,29 @@ EventEmitter.prototype.emit = function emit(type, ...args) {
216299
return false;
217300

218301
if (typeof handler === 'function') {
219-
ReflectApply(handler, this, args);
302+
const result = ReflectApply(handler, this, args);
303+
304+
// We check if result is undefined first because that
305+
// is the most common case so we do not pay any perf
306+
// penalty
307+
if (result !== undefined && result !== null) {
308+
addCatch(this, result, type, args);
309+
}
220310
} else {
221311
const len = handler.length;
222312
const listeners = arrayClone(handler, len);
223-
for (let i = 0; i < len; ++i)
224-
ReflectApply(listeners[i], this, args);
313+
for (var i = 0; i < len; ++i) {
314+
const result = ReflectApply(listeners[i], this, args);
315+
316+
// We check if result is undefined first because that
317+
// is the most common case so we do not pay any perf
318+
// penalty.
319+
// This code is duplicated because extracting it away
320+
// would make it non-inlineable.
321+
if (result !== undefined && result !== null) {
322+
addCatch(this, result, type, args);
323+
}
324+
}
225325
}
226326

227327
return true;

0 commit comments

Comments
 (0)