Skip to content

globalThis as an EventTarget #57352

Description

@jasnell

Browsers, Deno, Cloudflare Workers and other JavaScript runtimes have adopted the web platform standard of making globalThis extend from EventTarget and support events like unhandledreject, rejectionhandled, and error using the ErrorEvent, 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 using process and EventEmitter.

The WinterTC will be standardizing on globalThis extending from EventTarget and using the web platform standard events ... see https://github.com/wintercg/proposal-minimum-common-api/pull/82/files#diff-5e793325cd2bfc452e268a4aa2f02b4024dd9584bd1db3c2595f61f1ecf7b985R124

I would like us to revisit this and consider making globalThis and EventTarget and begin the slow, painful process of migrating away from process-based events, at least where there are existing web platform standard alternatives.

Why would we do this? Cross-runtime interop mainly.

@nodejs/tsc

Activity

  1. mcollina commented on Mar 6, 2025

    @mcollina
    SponsorMember

    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.

  2. jasnell commented on Mar 7, 2025

    @jasnell
    MemberAuthor

    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 error event. I think that if we went with something lke... If there is an 'error' handler on globalThis, emit that. Otherwise, propagate to `process.on('error')

  3. ljharb commented on Mar 7, 2025

    @ljharb
    SponsorMember

    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.on would silently fail to have their listeners triggered.

    iow, I think we'd probably have to have process.on just silently invoke globalThis.on, and have both work alongside each other.

  4. joyeecheung commented on Mar 7, 2025

    @joyeecheung
    Member

    begin the slow, painful process of migrating away from process-based events

    Can we not just coerce and emit the events on globalThis while we emit it on process too, or as @ljharb suggest, make process.on invoke globalThis.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 on globalThis and skipping process, and how much it's worth to break the ecosystem like that. Emitting the events on both sounds like a less disruptive path forward.

  5. jasnell commented on Mar 7, 2025

    @jasnell
    MemberAuthor

    ... 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...)

    1. 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 EventEmitter foo and it does not have an 'error' event handler, that gets bumped to process.on('error', ....) if it exists, and if it does not, then it is forwarded to process.on('uncaughtException', ...) or otherwise thrown as an uncaught exception. Where would globalThis.addEventListener('error', ...) fall within that flow? Or would it at all? If someone only as a globalThis.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?

    2. 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) process first then globalThis, (b) globalThis first then process, (c) only or the other? If option (b) and the user code requests to stop event propagation using, for instance, preventDefault() or stopImmediatePropagation() 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');
    1. If I call globalThis.dispatchEvent('error', new Error('foo')) would you expect both globalThis.addEventListener('error', ...) and process.on('error', ...) to be called? Likewise, if I called process.emit('error, ...) would you expect both the globalThis and process error handlers to called?

    2. When a developer ends up using a globalThis listener for, say, 'error' events but not a process listener, the process.listenerCount('error') would return 0, possibly giving the false impression that there are no error event handlers when there actually might be. But, then again, EventTarget does not give us a standard API to query handler counts which is why we made it so events.getEventListeners(...) can query both. Or... are we saying that when someone does globalThis.addEventListener('error', ...) that call will defer to process.on('error', ...) such that the process event handler will forward to the globalThis dispatcher?

  6. ljharb commented on Mar 7, 2025

    @ljharb
    SponsorMember

    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.

  7. benjamingr commented on Mar 7, 2025

    @benjamingr
    Member

    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.

  8. benjamingr commented on Mar 7, 2025

    @benjamingr
    Member

    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.stopImmediatePropagation on error prevent "uncaughtException" from firing? What order are events fired at? Does removing all listeners from uncaughtException impact error? How do we reconcile the different design decisions in EventTarget and EventEmitter without complicating the error handling story significantly for end users?

  9. ShogunPanda commented on Mar 8, 2025

    @ShogunPanda
    Contributor

    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 process and globalThis, 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 to process but the user can choose to switch to globalThis. 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 error as last.

  10. ljharb commented on Mar 8, 2025

    @ljharb
    SponsorMember

    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 process and parts to use globalThis.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    discussIssues opened for discussion and feedback.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions