denoland/deno · error · TypeError
MessageEvent constructor: Expected eventInitDict.source (${f
Error message
MessageEvent constructor: Expected eventInitDict.source (${formatForBranded(source)}) to be an instance of MessagePort. What it means
eventInitDict.source is typed in the spec as MessagePort | ServiceWorker | WindowProxy, but Deno has no JS-level brand for Window or ServiceWorker. The constructor therefore accepts anything except primitives and values whose prototype is exactly Object.prototype (ext/web/02_event.js:1405-1420): numbers, strings, booleans, and plain {} literals throw TypeError 'Expected eventInitDict.source ... to be an instance of MessagePort'. Real MessagePorts, the global object, and user-defined class instances pass through unchanged — a deliberate compromise to satisfy both WPT and Node's worker tests.
Source
Thrown at ext/web/02_event.js:1415
// ServiceWorker brand to gate against, so accept anything except
// primitives (which the node_compat tests expect to throw on,
// e.g. `source: 1`) and plain `Object` instances like `{}` (also
// checked by Node). MessagePort, Window-like globals, and
// user-defined classes all pass through unchanged -- matching both
// the WPT `messageevent-constructor.https.html` test (which
// assigns `window` to `source`) and node_compat
// `test-worker-message-event`.
const t = typeof source;
const isPrimitive = t !== "object" && t !== "function";
// Treat values whose prototype is exactly Object.prototype (i.e.
// plain object literals like `{}`) as invalid sources. Using the
// prototype chain rather than `.constructor` avoids the
// prefer-primordials lint and is more reliable since user code
// can override `constructor`.
const isPlainObject = !isPrimitive &&
ObjectGetPrototypeOf(source) === ObjectPrototype;
if (isPrimitive || isPlainObject) {
throw new TypeError(
`MessageEvent constructor: Expected eventInitDict.source (${
formatForBranded(source)
}) to be an instance of MessagePort.`,
);
}
this.#source = source;
} else {
this.#source = null;
}
}
[SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
return inspect(
getCreateFilteredInspectProxy()({
object: this,
evaluate: ObjectPrototypeIsPrototypeOf(MessageEventPrototype, this),
keys: [
...new SafeArrayIterator(EVENT_PROPS),View on GitHub (pinned to 9ad36f7a2c)
Solutions
- Set source to a real MessagePort (e.g. channel.port2) or omit it / pass null.
- Primitives and {} are rejected by design — delete the source field from fixtures instead of stubbing it.
- For window-like targets pass the global object itself (WPT assigns window), never a literal stand-in.
Example fix
// before
new MessageEvent('message', { data, source: {} });
// TypeError: Expected eventInitDict.source ([object Object]) to be an instance of MessagePort.
// after
new MessageEvent('message', { data, source: channel.port2 }); Defensive patterns
Strategy: type-guard
Validate before calling
const s = eventInit.source;
const ok = s == null ||
((typeof s === 'object' || typeof s === 'function') &&
Object.getPrototypeOf(s) !== Object.prototype);
if (!ok) delete eventInit.source;
new MessageEvent('message', eventInit); Type guard
function isMessageEventSource(v) {
if (v == null) return true;
if (typeof v !== 'object' && typeof v !== 'function') return false;
return Object.getPrototypeOf(v) !== Object.prototype; // plain {} rejected
} Prevention
- Use channel.port1/port2 as source
- Omit source instead of stubbing it with {} or a number in tests
- Remember the real rule: primitives and plain objects are rejected, class instances pass
When it happens
Trigger: new MessageEvent('message', { data, source: 1 }), { source: 'worker' }, or { source: {} } — primitives and plain object literals. source: null/undefined omitted, source: channel.port2, or source: globalThis are fine.
Common situations: Worker test fixtures setting source to a literal or a number (copied from Node test suites that assert the throw); forwarding a received event's source field that is actually plain data; TypeScript code assuming any object is accepted because the message mentions only MessagePort.
Related errors
- MessageEvent constructor: eventInitDict.ports (${formatForRa
- MessageEvent constructor: Expected eventInitDict.ports[${i}]
- BenchContext::start() has already been invoked
- ${prefix}Linter plugin name must only contain lowercase lett
- ${prefix}Linter plugin name must start and end with a lowerc
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/5ef11bc51a656555.
Report an issue: GitHub.