Repository navigation
globalThis as an EventTarget #57352
Description
Activity
I don’t think we’ll ever be able to migrate from process events to globalThis. We need to figure out a strategy where those can coexists.
Co-existence is possible, I think... so long as we can work out a strategy for the standard events that might occur on one or the other.
So, for instance, if we look at the
errorevent. I think that if we went with something lke... If there is an 'error' handler onglobalThis, emit that. Otherwise, propagate to `process.on('error')I'm not sure that can work - I think we'd have to fire both, otherwise packages that weren't updated to know about
globalThis.onwould silently fail to have their listeners triggered.iow, I think we'd probably have to have
process.onjust silently invokeglobalThis.on, and have both work alongside each other.begin the slow, painful process of migrating away from process-based events
Can we not just coerce and emit the events on
globalThiswhile we emit it onprocesstoo, or as @ljharb suggest, makeprocess.oninvokeglobalThis.on? Then it wouldn't need to be breaking - I doubt if we'll ever be able to break process event handlers by only emitting events onglobalThisand skippingprocess, and how much it's worth to break the ecosystem like that. Emitting the events on both sounds like a less disruptive path forward.Reacted by Marco Ippolito, Chengzhong Wu, Sebastian Beltran and Jordan Harband... make process.on invoke globalThis.on
Ok, let's go with this and let's see if we can define the expected behaviors a bit more... (even if we don't actually implement this Node.js, this discussion can actually be fairly useful for other runtimes that are seeking Node.js compatibility...)
-
One case where it gets a bit tricky is with
process.on('error', ...). The'error'events are the only ones we have that propagate up... that is, if I have an EventEmitterfooand it does not have an'error'event handler, that gets bumped toprocess.on('error', ....)if it exists, and if it does not, then it is forwarded toprocess.on('uncaughtException', ...)or otherwise thrown as an uncaught exception. Where wouldglobalThis.addEventListener('error', ...)fall within that flow? Or would it at all? If someone only as aglobalThis.addEventListener('error', ...)would that be enough to stop the propagation to an unhandled error or would the globalThis error handler not matter in the typical flow of an EventEmitter error? -
what would y'all expect the behavior to be if someone registers both process event and global event handlers? Which order should they be invoked in: (a)
processfirst thenglobalThis, (b)globalThisfirst thenprocess, (c) only or the other? If option (b) and the user code requests to stop event propagation using, for instance,preventDefault()orstopImmediatePropagation()should that prevent the process event handler from being fired?
// What order would you expect these to fire in? process.on('unhandledRejection', (...args) => console.log(1, ...args)); globalThis.addEventListener('unhandledrejection', (event) => console.log(2, event)); process.on('unhandledRejection', (...args) => console.log(3, ...args)); globalThis.addEventListener('unhandledrejection', (event) => console.log(4, event)); Promise.reject('foo');
-
If I call
globalThis.dispatchEvent('error', new Error('foo'))would you expect bothglobalThis.addEventListener('error', ...)andprocess.on('error', ...)to be called? Likewise, if I calledprocess.emit('error, ...)would you expect both theglobalThisandprocesserror handlers to called? -
When a developer ends up using a
globalThislistener for, say,'error'events but not aprocesslistener, theprocess.listenerCount('error')would return0, possibly giving the false impression that there are no error event handlers when there actually might be. But, then again,EventTargetdoes not give us a standard API to query handler counts which is why we made it soevents.getEventListeners(...)can query both. Or... are we saying that when someone doesglobalThis.addEventListener('error', ...)that call will defer toprocess.on('error', ...)such that the process event handler will forward to the globalThis dispatcher?
-
I’d consider globalThis the “parent”, so that it’s got the final say - meaning, process gets hit first, and then globalThis.
Alternatively we could treat them as the same event list, so eg listenerCount would include both globalThis and process events.
I see very little value in doing this for compatibility and this creates a mess of special mixing cases between process and globalThis events.
Namely: this further complicates our already complicated error handling story with more events and orders which can negatively impact usability. This is made worse by libraries possibly checking for globalThis being an event emitter as a potential probe and adjust their error handling accordingly (since the error model in browsers is different than our own).
I'm fine with a utility that "either returns globalThis if it's an EventEmitter or returns an EventEmitter that fires all the events on globalThis" as an alternative if the goal is making writing cross-compatible code easier.
Oh and btw regarding @joyeecheung 's suggestion:
Emitting the events on both sounds like a less disruptive path forward.
Unfortunately I think this can still be very disruptive @joyeecheung since it creates a lot of ambiguity. For example does
e.stopImmediatePropagationonerrorprevent "uncaughtException" from firing? What order are events fired at? Does removing all listeners fromuncaughtExceptionimpacterror? How do we reconcile the different design decisions inEventTargetandEventEmitterwithout complicating the error handling story significantly for end users?I think staying on track with WinterTC it is important, while we also have to preserve our and our users' mental health.
In principle I also liked the idea to emit on both
processandglobalThis, but I can see how quickly this can become tricky.Since I also agree with @mcollina that we will never get rid of
process, why don't we introduce a small "selecting API" that will allow the users to explicitly tell us where they want those events to be emitted? It defaults toprocessbut the user can choose to switch toglobalThis. Something like (name totally made and temporary):process.useGlobalThisForEvents().
Note that once the switch has made, their is purposely no API to switch back (at least for now).I know it will make implementation a little bit harder, but it will also allow us to test with various event gradually, for instance by leaving hard one like
erroras last.Something global won't work with a combination of packages and application code - it has to always be acceptable for parts of the application to use
processand parts to useglobalThis.Reacted by ExE Boss- addeddiscussIssues opened for discussion and feedback.Issues opened for discussion and feedback.
on Mar 24, 2025
Browsers, Deno, Cloudflare Workers and other JavaScript runtimes have adopted the web platform standard of making
globalThisextend fromEventTargetand support events likeunhandledreject,rejectionhandled, anderrorusing theErrorEvent,PromiseRejectionEvent, etc APIs. Node.js, on the other hand, for legacy reasons does not have these and instead offers it's own variation on these events usingprocessandEventEmitter.The WinterTC will be standardizing on
globalThisextending fromEventTargetand using the web platform standard events ... see https://github.com/wintercg/proposal-minimum-common-api/pull/82/files#diff-5e793325cd2bfc452e268a4aa2f02b4024dd9584bd1db3c2595f61f1ecf7b985R124I would like us to revisit this and consider making
globalThisandEventTargetand begin the slow, painful process of migrating away fromprocess-based events, at least where there are existing web platform standard alternatives.Why would we do this? Cross-runtime interop mainly.
@nodejs/tsc