Skip to content

EventEmitter.prototype methods (emit/on/…) are undefined — breaks the setPrototypeOf mixin pattern (pino) #5477

Description

@proggeramlug

Summary

EventEmitter.prototype.emit (and .on, .once, etc.) resolve to undefined, even though EventEmitter instances dispatch those methods fine. So the common "mixin EventEmitter into a prototype" pattern — Object.setPrototypeOf(myProto, EventEmitter.prototype) — produces objects with no emit.

This breaks pino (one of the most-used npm loggers): lib/proto.js:77 does Object.setPrototypeOf(prototype, EventEmitter.prototype), then lib/levels.js:98 calls this.emit('level-change', …)TypeError: emit is not a function (located via --debug-symbols).

Minimal reproduction

import { EventEmitter } from "node:events"

console.log("EE.prototype.emit:", typeof (EventEmitter.prototype as any).emit)  // undefined  (Node: function)
console.log("EE.prototype.on:",   typeof (EventEmitter.prototype as any).on)    // undefined  (Node: function)

const e: any = new EventEmitter()
console.log("instance.emit:", typeof e.emit)                                     // function  ✅ (instances work)
console.log("getProto(instance) === EE.prototype:", Object.getPrototypeOf(e) === EventEmitter.prototype) // true
console.log("getProto(instance).emit:", typeof Object.getPrototypeOf(e).emit)    // undefined ❌

// the pino mixin shape:
const proto: any = { foo() {} }
Object.setPrototypeOf(proto, EventEmitter.prototype)
const inst: any = Object.create(proto)
console.log("mixin inst.emit:", typeof inst.emit)                                // undefined ❌ (Node: function)

Root cause / context

This is the next layer beyond #5269 (which made native ctor-class exports expose a real .prototype object). The .prototype object exists but its methods aren't accessible on it: EventEmitter is handle-based (EventEmitterHandle / js_event_emitter_new), and instance emit/on dispatch natively keyed on the instance handle — but a plain property GET of emit on the prototype object resolves nothing.

class X extends EventEmitter {} already works (subclass instances dispatch emit), so the "non-direct-EventEmitter object behaves as an emitter" machinery exists for the subclass path; the setPrototypeOf mixin path doesn't hook into it.

What a fix needs

  1. EventEmitter.prototype.emit/.on/… resolve to the native methods (so the property GET / proto-chain lookup finds them).
  2. Those methods work with an arbitrary this (a plain object whose proto chain includes EventEmitter.prototype), i.e. lazily attach emitter state on first on/emit — mirroring how class … extends EventEmitter instances become emitters. Without (2), exposing the methods just moves the error to "native emit on a non-emitter this".

Impact

pino, and any library using the setPrototypeOf(proto, EventEmitter.prototype) mixin (a common pattern). Distinct from #5268 (that was the Object.create/__proto__ proto-resolves-to-undefined crash, fixed by #5269; pino now loads past it and fails here instead).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions