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
EventEmitter.prototype.emit/.on/… resolve to the native methods (so the property GET / proto-chain lookup finds them).
- 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).
Summary
EventEmitter.prototype.emit(and.on,.once, etc.) resolve toundefined, even thoughEventEmitterinstances dispatch those methods fine. So the common "mixin EventEmitter into a prototype" pattern —Object.setPrototypeOf(myProto, EventEmitter.prototype)— produces objects with noemit.This breaks pino (one of the most-used npm loggers):
lib/proto.js:77doesObject.setPrototypeOf(prototype, EventEmitter.prototype), thenlib/levels.js:98callsthis.emit('level-change', …)→TypeError: emit is not a function(located via--debug-symbols).Minimal reproduction
Root cause / context
This is the next layer beyond #5269 (which made native ctor-class exports expose a real
.prototypeobject). The.prototypeobject exists but its methods aren't accessible on it:EventEmitteris handle-based (EventEmitterHandle/js_event_emitter_new), and instanceemit/ondispatch natively keyed on the instance handle — but a plain property GET ofemiton the prototype object resolves nothing.class X extends EventEmitter {}already works (subclass instances dispatchemit), so the "non-direct-EventEmitter object behaves as an emitter" machinery exists for the subclass path; thesetPrototypeOfmixin path doesn't hook into it.What a fix needs
EventEmitter.prototype.emit/.on/… resolve to the native methods (so the property GET / proto-chain lookup finds them).this(a plain object whose proto chain includesEventEmitter.prototype), i.e. lazily attach emitter state on firston/emit— mirroring howclass … extends EventEmitterinstances become emitters. Without (2), exposing the methods just moves the error to "native emit on a non-emitterthis".Impact
pino, and any library using the
setPrototypeOf(proto, EventEmitter.prototype)mixin (a common pattern). Distinct from #5268 (that was theObject.create/__proto__proto-resolves-to-undefined crash, fixed by #5269; pino now loads past it and fails here instead).