| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 35ce8bb commit 4265d57
3 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -155,9 +155,66 @@ myEmitter.emit('error', new Error('whoops!')); | |||
| 155 | 155 | // Prints: whoops! there was an error | |
| 156 | 156 | ``` | |
| 157 | 157 | ||
| 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 | + | ||
| 158 | 211 | ## Class: `EventEmitter` | |
| 159 | 212 | <!-- YAML | |
| 160 | 213 | added: v0.1.26 | |
| 214 | + changes: | ||
| 215 | + - version: REPLACEME | ||
| 216 | + pr-url: https://github.com/nodejs/node/pull/27867 | ||
| 217 | + description: Added captureRejections option. | ||
| 161 | 218 | --> | |
| 162 | 219 | ||
| 163 | 220 | The `EventEmitter` class is defined and exposed by the `events` module: | |
@@ -169,6 +226,12 @@ const EventEmitter = require('events'); | |||
| 169 | 226 | All `EventEmitter`s emit the event `'newListener'` when new listeners are | |
| 170 | 227 | added and `'removeListener'` when existing listeners are removed. | |
| 171 | 228 | ||
| 229 | + It supports the following option: | ||
| 230 | + | ||
| 231 | + * `captureRejections` {boolean} It enables | ||
| 232 | + [automatic capturing of promise rejection][capturerejections]. | ||
| 233 | + Default: `false`. | ||
| 234 | + | ||
| 172 | 235 | ### Event: 'newListener' | |
| 173 | 236 | <!-- YAML | |
| 174 | 237 | added: v0.1.26 | |
@@ -694,6 +757,42 @@ newListeners[0](); | |||
| 694 | 757 | emitter.emit('log'); | |
| 695 | 758 | ``` | |
| 696 | 759 | ||
| 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 | + | ||
| 697 | 796 | ## `events.once(emitter, name)` | |
| 698 | 797 | <!-- YAML | |
| 699 | 798 | added: v11.13.0 | |
@@ -740,6 +839,28 @@ async function run() { | |||
| 740 | 839 | run(); | |
| 741 | 840 | ``` | |
| 742 | 841 | ||
| 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 | + | ||
| 743 | 864 | [WHATWG-EventTarget]: https://dom.spec.whatwg.org/#interface-eventtarget | |
| 744 | 865 | [`--trace-warnings`]: cli.html#cli_trace_warnings | |
| 745 | 866 | [`EventEmitter.defaultMaxListeners`]: #events_eventemitter_defaultmaxlisteners | |
@@ -751,3 +872,7 @@ run(); | |||
| 751 | 872 | [`net.Server`]: net.html#net_class_net_server | |
| 752 | 873 | [`process.on('warning')`]: process.html#process_event_warning | |
| 753 | 874 | [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 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -23,6 +23,7 @@ | |||
| 23 | 23 | ||
| 24 | 24 | const { | |
| 25 | 25 | Array, | |
| 26 | + Boolean, | ||
| 26 | 27 | Error, | |
| 27 | 28 | MathMin, | |
| 28 | 29 | NumberIsNaN, | |
@@ -33,7 +34,10 @@ const { | |||
| 33 | 34 | Promise, | |
| 34 | 35 | ReflectApply, | |
| 35 | 36 | ReflectOwnKeys, | |
| 37 | + Symbol, | ||
| 38 | + SymbolFor, | ||
| 36 | 39 | } = primordials; | |
| 40 | + const kRejection = SymbolFor('nodejs.rejection'); | ||
| 37 | 41 | ||
| 38 | 42 | let spliceOne; | |
| 39 | 43 | ||
@@ -51,8 +55,10 @@ const { | |||
| 51 | 55 | inspect | |
| 52 | 56 | } = require('internal/util/inspect'); | |
| 53 | 57 | ||
| 54 | - function EventEmitter() { | ||
| 55 | - EventEmitter.init.call(this); | ||
| 58 | + const kCapture = Symbol('kCapture'); | ||
| 59 | + | ||
| 60 | + function EventEmitter(opts) { | ||
| 61 | + EventEmitter.init.call(this, opts); | ||
| 56 | 62 | } | |
| 57 | 63 | module.exports = EventEmitter; | |
| 58 | 64 | module.exports.once = once; | |
@@ -62,6 +68,29 @@ EventEmitter.EventEmitter = EventEmitter; | |||
| 62 | 68 | ||
| 63 | 69 | EventEmitter.usingDomains = false; | |
| 64 | 70 | ||
| 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 | + | ||
| 65 | 94 | EventEmitter.prototype._events = undefined; | |
| 66 | 95 | EventEmitter.prototype._eventsCount = 0; | |
| 67 | 96 | EventEmitter.prototype._maxListeners = undefined; | |
@@ -91,7 +120,7 @@ ObjectDefineProperty(EventEmitter, 'defaultMaxListeners', { | |||
| 91 | 120 | } | |
| 92 | 121 | }); | |
| 93 | 122 | ||
| 94 | - EventEmitter.init = function() { | ||
| 123 | + EventEmitter.init = function(opts) { | ||
| 95 | 124 | ||
| 96 | 125 | if (this._events === undefined || | |
| 97 | 126 | this._events === ObjectGetPrototypeOf(this)._events) { | |
@@ -100,8 +129,64 @@ EventEmitter.init = function() { | |||
| 100 | 129 | } | |
| 101 | 130 | ||
| 102 | 131 | 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 | + } | ||
| 103 | 145 | }; | |
| 104 | 146 | ||
| 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 | + | ||
| 105 | 190 | // Obviously not all Emitters should be limited to 10. This function allows | |
| 106 | 191 | // that to be increased. Set to zero for unlimited. | |
| 107 | 192 | EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { | |
@@ -218,12 +303,29 @@ EventEmitter.prototype.emit = function emit(type, ...args) { | |||
| 218 | 303 | return false; | |
| 219 | 304 | ||
| 220 | 305 | 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 | + } | ||
| 222 | 314 | } else { | |
| 223 | 315 | const len = handler.length; | |
| 224 | 316 | 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 | + } | ||
| 227 | 329 | } | |
| 228 | 330 | ||
| 229 | 331 | return true; | |
| Back | FazBrowse Home | New Git URL |
0 commit comments