Skip to content

Commit 4265d57

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 35ce8bb commit 4265d57

File tree

3 files changed

+530
-6
lines changed

3 files changed

+530
-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

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

2424
const {
2525
Array,
26+
Boolean,
2627
Error,
2728
MathMin,
2829
NumberIsNaN,
@@ -33,7 +34,10 @@ const {
3334
Promise,
3435
ReflectApply,
3536
ReflectOwnKeys,
37+
Symbol,
38+
SymbolFor,
3639
} = primordials;
40+
const kRejection = SymbolFor('nodejs.rejection');
3741

3842
let spliceOne;
3943

@@ -51,8 +55,10 @@ const {
5155
inspect
5256
} = require('internal/util/inspect');
5357

54-
function EventEmitter() {
55-
EventEmitter.init.call(this);
58+
const kCapture = Symbol('kCapture');
59+
60+
function EventEmitter(opts) {
61+
EventEmitter.init.call(this, opts);
5662
}
5763
module.exports = EventEmitter;
5864
module.exports.once = once;
@@ -62,6 +68,29 @@ EventEmitter.EventEmitter = EventEmitter;
6268

6369
EventEmitter.usingDomains = false;
6470

71+
EventEmitter.captureRejectionSymbol = kRejection;
72+
ObjectDefineProperty(EventEmitter, 'captureRejections', {
73+
get() {
74+
return EventEmitter.prototype[kCapture];
75+
},
76+
set(value) {
77+
if (typeof value !== 'boolean') {
78+
throw new ERR_INVALID_ARG_TYPE('EventEmitter.captureRejections',
79+
'boolean', value);
80+
}
81+
82+
EventEmitter.prototype[kCapture] = value;
83+
},
84+
enumerable: true
85+
});
86+
87+
// The default for captureRejections is false
88+
ObjectDefineProperty(EventEmitter.prototype, kCapture, {
89+
value: false,
90+
writable: true,
91+
enumerable: false
92+
});
93+
6594
EventEmitter.prototype._events = undefined;
6695
EventEmitter.prototype._eventsCount = 0;
6796
EventEmitter.prototype._maxListeners = undefined;
@@ -91,7 +120,7 @@ ObjectDefineProperty(EventEmitter, 'defaultMaxListeners', {
91120
}
92121
});
93122

94-
EventEmitter.init = function() {
123+
EventEmitter.init = function(opts) {
95124

96125
if (this._events === undefined ||
97126
this._events === ObjectGetPrototypeOf(this)._events) {
@@ -100,8 +129,64 @@ EventEmitter.init = function() {
100129
}
101130

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

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

220305
if (typeof handler === 'function') {
221-
ReflectApply(handler, this, args);
306+
const result = ReflectApply(handler, this, args);
307+
308+
// We check if result is undefined first because that
309+
// is the most common case so we do not pay any perf
310+
// penalty
311+
if (result !== undefined && result !== null) {
312+
addCatch(this, result, type, args);
313+
}
222314
} else {
223315
const len = handler.length;
224316
const listeners = arrayClone(handler, len);
225-
for (let i = 0; i < len; ++i)
226-
ReflectApply(listeners[i], this, args);
317+
for (var i = 0; i < len; ++i) {
318+
const result = ReflectApply(listeners[i], this, args);
319+
320+
// We check if result is undefined first because that
321+
// is the most common case so we do not pay any perf
322+
// penalty.
323+
// This code is duplicated because extracting it away
324+
// would make it non-inlineable.
325+
if (result !== undefined && result !== null) {
326+
addCatch(this, result, type, args);
327+
}
328+
}
227329
}
228330

229331
return true;

0 commit comments

Comments
 (0)