denoland/deno · error · TypeError
MessageEvent constructor: eventInitDict.ports (${formatForRa
Error message
MessageEvent constructor: eventInitDict.ports (${formatForRaw(ports)}) is not iterable. What it means
The MessageEvent constructor validates eventInitDict.ports, typed in the HTML spec as FrozenArray<MessagePort>. Any non-null value must be an object exposing Symbol.iterator (ext/web/02_event.js:1348-1357); anything else throws TypeError '... is not iterable'. The wording deliberately matches Node's assertion messages so node_compat tests pass while the shape stays WHATWG. The classic trigger is passing a single MessagePort instead of an array containing it.
Source
Thrown at ext/web/02_event.js:1354
cancelable: eventInitDict?.cancelable ?? false,
composed: eventInitDict?.composed ?? false,
});
this.data = eventInitDict?.data ?? null;
const ports = eventInitDict?.ports;
if (ports == null) {
// `ports` is a FrozenArray<MessagePort> per the HTML spec, so the
// exposed array must be read-only.
this.ports = ObjectFreeze([]);
} else {
// MessageEvent ports: iterable validation + per-element MessagePort
// type check. Matches the messages Node asserts on so the
// node_compat tests pass while still being WHATWG-shaped.
if (
ports === null || typeof ports !== "object" ||
ports[SymbolIterator] === undefined
) {
throw new TypeError(
`MessageEvent constructor: eventInitDict.ports (${
formatForRaw(ports)
}) is not iterable.`,
);
}
const MessagePortProto = getMessagePortPrototype();
const arr = [];
let i = 0;
// Iterate using the user's own iterator so values that aren't real
// arrays (e.g. a `RegExp` with a custom `Symbol.iterator` -- covered
// by WPT's no-regexp-special-casing test) still produce the
// expected `ports` array. SafeArrayIterator can't be used here
// because it walks the value as if it were an Array.
// deno-lint-ignore deno-internal/prefer-primordials
for (const p of ports) {
if (
p === null || typeof p !== "object" ||
!ObjectPrototypeIsPrototypeOf(MessagePortProto, p)View on GitHub (pinned to 9ad36f7a2c)
Solutions
- Wrap ports in an array: new MessageEvent('message', { data, ports: [channel.port1] }).
- Omit ports (or pass null/undefined) when no ports were transferred — the constructor freezes an empty array.
- Validate third-party payloads before constructing: check ports == null || (typeof ports === 'object' && typeof ports[Symbol.iterator] === 'function').
Example fix
// before
new MessageEvent('message', { data, ports: channel.port1 });
// TypeError: MessageEvent constructor: eventInitDict.ports ([object MessagePort]) is not iterable.
// after
new MessageEvent('message', { data, ports: [channel.port1] }); Defensive patterns
Strategy: type-guard
Validate before calling
const raw = eventInit.ports;
if (raw != null && (typeof raw !== 'object' || typeof raw[Symbol.iterator] !== 'function')) {
throw new TypeError('eventInitDict.ports must be an array of MessagePort');
}
new MessageEvent('message', { ...eventInit, ports: raw == null ? [] : [...raw] }); Type guard
function isPortList(v) {
return v == null ||
(typeof v === 'object' && typeof v[Symbol.iterator] === 'function');
} Prevention
- Always wrap ports in an array: ports: [port]
- Omit ports when none were transferred
- Pass only web MessagePorts from MessageChannel or worker onmessage
When it happens
Trigger: new MessageEvent('message', { data, ports: channel.port1 }) — a single MessagePort is an object but has no Symbol.iterator; also ports: 'abc', ports: 123, or ports: {} (no iterator). ports omitted, null, or an iterable array are accepted.
Common situations: Transferring exactly one port and passing it unwrapped; test fixtures for postMessage/messagepassing written by Node developers (Node tolerates different shapes); polyfills that accept either a port or an array and forward verbatim.
Related errors
- MessageEvent constructor: Expected eventInitDict.ports[${i}]
- MessageEvent constructor: Expected eventInitDict.source (${f
- 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/e38751c7e102bcae.
Report an issue: GitHub.