Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 4265d57

Browse filesBrowse files
mcollinatargos
authored andcommitted
events: add captureRejection option
PR-URL: #27867 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Michaël Zasso <targos@protonmail.com>
1 parent 35ce8bb commit 4265d57
Copy full SHA for 4265d57

File tree

Expand file treeCollapse file tree

3 files changed

+530
-6
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

3 files changed

+530
-6
lines changed
Open diff view settings
Collapse file

‎doc/api/events.md‎

Copy file name to clipboardExpand all lines: doc/api/events.md
+125Lines changed: 125 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
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
Collapse file

‎lib/events.js‎

Copy file name to clipboardExpand all lines: lib/events.js
+108-6Lines changed: 108 additions & 6 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.