Skip to content

Commit 0a773db

Browse files
committed
events: allow monitoring error events
Installing an error listener has a side effect that emitted errors are considered as handled. This is quite bad for monitoring/logging tools which tend to be interested in errors but don't want to cause side effects like swallow an exception. There are some workarounds in the wild like monkey patching emit or remit the error if monitoring tool detects that it is the only listener but this is error prone and risky. This PR allows to install a listener to monitor errors with the side effect to consume the error. To avoid conflicts with other events it exports a symbol on EventEmitter which owns this special meaning. Refs: open-telemetry/opentelemetry-js#225
1 parent edf654d commit 0a773db

File tree

3 files changed

+74
-2
lines changed

3 files changed

+74
-2
lines changed

doc/api/events.md

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

158+
It is possible to monitor `'error'` events without consuming the emitted error
159+
by installing a listener using the symbol `errorMonitorSymbol`.
160+
161+
```js
162+
const myEmitter = new MyEmitter();
163+
myEmitter.on(errorMonitorSymbol, (err) => {
164+
MyMonitoringTool.log(err);
165+
});
166+
myEmitter.emit('error', new Error('whoops!'));
167+
// Still throws and crashes Node.js
168+
```
169+
158170
## Capture Rejections of Promises
159171

160172
> Stability: 1 - captureRejections is experimental.
@@ -348,6 +360,19 @@ the event emitter instance, the event’s name and the number of attached
348360
listeners, respectively.
349361
Its `name` property is set to `'MaxListenersExceededWarning'`.
350362

363+
### EventEmitter.errorMonitorSymbol
364+
<!-- YAML
365+
added: REPLACEME
366+
-->
367+
368+
This symbol shall be used to install a listener only monitoring `'error'`
369+
events. Listeners installed using this symbol are called before the regular
370+
`'error'` listeners are called.
371+
372+
Installing a listener using this symbol does not change the behavior once a
373+
`'error'` event is emitted, therefore the process will still crash if no
374+
regular `'error'` listener is installed.
375+
351376
### emitter.addListener(eventName, listener)
352377
<!-- YAML
353378
added: v0.1.26

lib/events.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const {
5454
} = require('internal/util/inspect');
5555

5656
const kCapture = Symbol('kCapture');
57+
const kErrorMonitorSymbol = Symbol('events.error-monitor');
5758

5859
function EventEmitter(opts) {
5960
EventEmitter.init.call(this, opts);
@@ -82,6 +83,13 @@ ObjectDefineProperty(EventEmitter, 'captureRejections', {
8283
enumerable: true
8384
});
8485

86+
ObjectDefineProperty(EventEmitter, 'errorMonitorSymbol', {
87+
value: kErrorMonitorSymbol,
88+
writable: false,
89+
configurable: true,
90+
enumerable: true
91+
});
92+
8593
// The default for captureRejections is false
8694
ObjectDefineProperty(EventEmitter.prototype, kCapture, {
8795
value: false,
@@ -255,9 +263,11 @@ EventEmitter.prototype.emit = function emit(type, ...args) {
255263
let doError = (type === 'error');
256264

257265
const events = this._events;
258-
if (events !== undefined)
266+
if (events !== undefined) {
267+
if (doError && events[kErrorMonitorSymbol] !== undefined)
268+
this.emit(kErrorMonitorSymbol, ...args);
259269
doError = (doError && events.error === undefined);
260-
else if (!doError)
270+
} else if (!doError)
261271
return false;
262272

263273
// If there is no 'error' event listener then throw.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const EventEmitter = require('events');
5+
6+
const EE = new EventEmitter();
7+
const theErr = new Error('MyError');
8+
9+
EE.on(
10+
EventEmitter.errorMonitorSymbol,
11+
common.mustCall((e) => assert.strictEqual(e, theErr), 3)
12+
);
13+
14+
// Verify with no error listener
15+
common.expectsError(
16+
() => EE.emit('error', theErr), theErr
17+
);
18+
19+
// Verify with error listener
20+
EE.once('error', common.mustCall((e) => assert.strictEqual(e, theErr)));
21+
EE.emit('error', theErr);
22+
23+
24+
// Verify it works with once
25+
process.nextTick(() => EE.emit('error', theErr));
26+
async function testOnce() {
27+
try {
28+
await EventEmitter.once(EE, 'notTriggered');
29+
} catch (e) {
30+
assert.strictEqual(e, theErr);
31+
}
32+
}
33+
testOnce();
34+
35+
// Only error events trigger error monitor
36+
EE.on('aEvent', common.mustCall());
37+
EE.emit('aEvent');

0 commit comments

Comments
 (0)