Error: A Node.js API is used (MessageChannel) which is not supported in the Edge Runtime.
Describe the Bug
Using React 19 + Next.js 15
I want to use @react-email/render on edge runtime. However, I got this error:
unhandledRejection: Error: A Node.js API is used (MessageChannel) which is not supported in the Edge Runtime.
Which package is affected (leave empty if unsure)
@react-email/render
Link to the code that reproduces this issue
https://codesandbox.io/p/devbox/react-email-render-on-edge-runtime-5dlkxp
To Reproduce
- Go to
/api/email - See the error
Expected Behavior
No errors. And render the component successfully in email.
What's your node version? (if relevant)
No response
Not 100% sure we can do something about this as react-dom, which we use internally, seems to be the one using MessageChannel here. All the other APIs we use are supported on the edge without issues.
I've also noticed that the issue goes away when downgrading the React version and Next version to latest, so I do believe we will have support for MessageChannel and this issue going away soon. That being said, we should keep the issue open until it is fixed on Next. I suppose you can also downgrade React to an RC that did not use MessageChannel yet.
@gabrielmfern , I’m not sure if supporting the Edge runtime as a separate environment, alongside browser and Node, is within the scope of react-email. However, if it is, you might find it helpful to review how the Next.js team addressed this issue here.
They used the react-dom/server.edge import to ensure compatibility between React and the Edge runtime.
I’m not sure if supporting the Edge runtime as a separate environment, alongside browser and Node, is within the scope of react-email They used the react-dom/server.edge import to ensure compatibility between React and the Edge runtime.
Definitely within the scope. Thanks for letting us know about the react-dom/server.edge export, but that does seem to be exclusive to the newer RCs, so I don't think we can really use it yet until we remove support for 18.x.
On another note, seems like Next 15 with edge routes also have this error when using the latest @react-email/render. Another possibility is that we can attempt at importing the edge version and fallback to other versions if it fails. Might need some finagling, but we'll need a way to have both versions working at the same time.
Wrote my own render function using react-dom/server.edge
import type { ReactDOMServerReadableStream } from "react-dom/server";
const readStream = async (stream: ReactDOMServerReadableStream) => {
let result = "";
// means it's a readable stream
const writableStream = new WritableStream({
write(chunk) {
result += new TextDecoder("utf-8").decode(chunk);
},
});
await stream.pipeTo(writableStream);
return result;
};
export const renderEdge = async (element: React.ReactNode) => {
const renderToReadableStream = (await import("react-dom/server.edge"))
.renderToReadableStream;
const r = await renderToReadableStream(element);
const html = await readStream(r);
const doctype =
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
const document = `${doctype}${html.replace(/<!DOCTYPE.*?>/, "")}`;
return document;
};
It solves the problem but I encountered a new issue instead:
TypeError: Cannot read properties of undefined (reading 'H')
It seems like React mail is not supporting NextJS 15 unfortunately. I'm not sure how to get past this.
In any case If it's complicated to support edge in the same render function due to backwards compatibility I would't mind having a separate render function for edge runtime. :)
@cjkihl Noticed the same error when experimenting with the export. Looking at the compiled code, I am pretty sure this is because Next does its thing with pre-compiled React versions while still involving the user's React version, might be a bug on their side.
Made an issue about the error when trying to render with react-dom/server.edge https://github.com/vercel/next.js/issues/71865
Based on what I get from https://github.com/vercel/next.js/issues/71865#issuecomment-2444377726, this issue is probably only going to be fully fixed once we get react-markup fully implemented and released, unless we might adopt it while it is still experimental
So I just got this issue today myself, but I've been using Pages Router and it wasn't happening until I tried to refactor to use App Router instead. In both scenarios, I have the function set to use Edge Runtime.
Is everybody here only using App Router? If so, does the issue resolve if you switch to Pages Router?
Edit: I just realized that somebody on the Next team confirmed this on the issue that @gabrielmfern logged there. So if you don't mind switching to Pages Router, that seems to be a temporary workaround.
Is there a timeline for react-markup anywhere?
@dualdetail Not that I know of, I don't think that they even announced it anywhere yet. But I assume it might release around the time React 19 is released.
Not 100% sure we can do something about this as
react-dom, which we use internally, seems to be the one using MessageChannel here. All the other APIs we use are supported on the edge without issues.I've also noticed that the issue goes away when downgrading the React version and Next version to latest, so I do believe we will have support for MessageChannel and this issue going away soon. That being said, we should keep the issue open until it is fixed on Next. I suppose you can also downgrade React to an RC that did not use MessageChannel yet.
Stable versions of both React@19 and Next@15 are released, but this issue still exists, are there any fixes yet?
@raodevendrasingh https://github.com/resend/react-email/issues/1630#issuecomment-2450289005
Our project was recently migrated from React 18 & Next.js 14 to React 19 & Next.js 15. However, we're now stuck on sending emails due to this issue.
Has anyone found a workaround for this issue while waiting for the official fix or react-markup release? Any insights would be greatly appreciated!
I faced exactly same issue while deploying my project to cloudflare.
I am getting error as ReferenceError: MessageChannel is not defined
I am using NextJS 15 with latest react version.
Also hitting the same issue. Looks like we have to wait for Cloudflare support https://github.com/cloudflare/workerd/pull/3336
I'm also encountering the "MessageChannel is not defined" error when using React DOM 19.0.0 in a server environment (Convex):
[Request ID: 634ac687fb526c49] Server Error
Uncaught ReferenceError: MessageChannel is not defined
at <anonymous> (../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom-server.browser.development.js:7332:42)
at node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom-server.browser.development.js (../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom-server.browser.development.js:8955:5)
at __require (convex:/_deps/UZXTZDBD.js:9:50)
at node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/server.browser.js (../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/server.browser.js:9:0)
at __require (convex:/_deps/UZXTZDBD.js:9:50)
at <anonymous> (../../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/server.browser.js:17:0)
Environment:
- React: 19.0.0
- React DOM: 19.0.0
- Package manager: pnpm
- Environment: Convex backend
The error suggests that the browser-specific MessageChannel API is being referenced in a server environment where it's not available. This appears to be happening when attempting to use the react-dom/server module.
Has anyone else found a workaround? I'd appreciate any guidance on how to resolve this.
im facing the same issue as @noobships . totally same environment
Possible work around is to run a prebuild script to regenerate the function with the html prerendered.
tsx scripts/prerender-emails.ts
// scripts/prerender-emails.ts
import fs from "fs";
import path from "path";
import { pathToFileURL } from "url";
import { render } from "@react-email/render";
const templatesDir = path.resolve(__dirname, "../emails/templates");
const outputDir = path.resolve(__dirname, "../emails/html");
interface TemplateModule {
default: React.FC;
props?: Record<string, string>;
}
/**
* Converts a kebab-case or snake_case string to PascalCase
* @param str The string to convert
* @returns The PascalCase string
*/
const toPascalCase = (str: string): string =>
str
.replace(/[-_](.)/g, (_, group1) => group1.toUpperCase())
.replace(/^(.)/, (_, group1) => group1.toUpperCase());
/**
* Converts a PascalCase string to camelCase
* @param str The PascalCase string to convert
* @returns The camelCase string
*/
const toCamelCase = (str: string): string =>
str.charAt(0).toLowerCase() + str.slice(1);
async function prerenderTemplates() {
/**
* Prerenders React Email templates to HTML files with placeholder values.
*
* This script:
* 1. Reads all .tsx template files from the templates directory
* 2. Imports and renders each template with placeholder props
* 3. Generates HTML files with template literal placeholders
* 4. Saves the HTML files to the output directory
*
* The generated HTML files can then be used with template literals
* to inject real values at runtime.
*/
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const files = fs.readdirSync(templatesDir).filter((f) => f.endsWith(".tsx"));
for (const file of files) {
const baseName = file.replace(/\.tsx$/, "");
const pascalName = toPascalCase(baseName);
const camelName = toCamelCase(pascalName);
const modulePath = pathToFileURL(path.join(templatesDir, file)).href;
let mod: TemplateModule;
try {
mod = (await import(modulePath)) as TemplateModule;
} catch (err) {
console.error(`❌ Failed to import ${file}:`, err);
continue;
}
const Component = mod.default;
const props = mod.props ?? {};
if (!Component) {
console.warn(`⚠️ Skipping ${file} — no default export found.`);
continue;
}
let html: string;
try {
html = await render(Component(props) as React.ReactElement);
} catch (err) {
console.error(`❌ Failed to render ${file}:`, err);
continue;
}
const escapedHtml = html.replace(/`/g, "\\`");
const propKeys = Object.keys(props);
const destructure = propKeys.length
? `const { ${propKeys
.map((k) => `${k} = "${props[k]}"`)
.join(", ")} } = props;`
: "";
let htmlWithPlaceholders = escapedHtml;
for (const key of propKeys) {
htmlWithPlaceholders = htmlWithPlaceholders.replace(
new RegExp(`{{${key}}}`, "g"),
`\${${key}}`,
);
}
const templateFn = `// ⚠️ Auto-generated by scripts/prerender-emails.ts
export const ${camelName}Template = (props: Record<string, string | number> = {}): string => {
${destructure}
return \`${htmlWithPlaceholders}\`;
};
`;
const outputFilePath = path.join(outputDir, `${pascalName}.ts`);
fs.writeFileSync(outputFilePath, templateFn, "utf8");
console.log(
`✅ Generated: ${path.relative(process.cwd(), outputFilePath)}`,
);
}
}
prerenderTemplates().catch((err) => {
console.error("Unhandled error in prerendering:", err);
process.exit(1);
});
// emails/utils.ts
export const definePlaceholderProps = <T>(props: Record<keyof T, string>) =>
props;
// emails/templates/verification-code.tsx
import React from "react";
import {
Html,
Tailwind,
Head,
Container,
Section,
Heading,
Text,
} from "@react-email/components";
import { definePlaceholderProps } from "../utils";
export type VerificationCodeProps = {
token: string;
minutesUntilExpiry: string | number;
};
export const props = definePlaceholderProps<VerificationCodeProps>({
token: "{{token}}",
minutesUntilExpiry: "{{minutesUntilExpiry}}",
});
export default function VerificationCodeEmail({
token,
minutesUntilExpiry,
}: VerificationCodeProps) {
return (
<Html>
<Tailwind>
<Head />
<Container className="px-20 font-sans">
<Heading className="text-xl font-bold mb-4">
Sign in to My App
</Heading>
<Text className="text-sm">Please enter the following code:</Text>
<Section className="text-center">
<Text className="font-semibold">Verification code</Text>
<Text className="font-bold text-4xl">{token}</Text>
<Text>(This code is valid for {minutesUntilExpiry} minutes)</Text>
</Section>
</Container>
</Tailwind>
</Html>
);
}
You can polyfill MessageChannel, it doesn't appear to be actually used. Can render emails in Convex runtime (a web-like runtime similar to Edge) with React 19 using this polyfill and the patch outlined below. A more minimal polyfill may work, didn't test that far.
if (typeof MessageChannel === "undefined") {
class MockMessagePort {
onmessage: ((ev: MessageEvent) => void) | undefined;
onmessageerror: ((ev: MessageEvent) => void) | undefined;
close() {}
postMessage(_message: unknown, _transfer: Transferable[] = []) {}
start() {}
addEventListener() {}
removeEventListener() {}
dispatchEvent(_event: Event): boolean {
return false;
}
}
class MockMessageChannel {
port1: MockMessagePort;
port2: MockMessagePort;
constructor() {
this.port1 = new MockMessagePort();
this.port2 = new MockMessagePort();
}
}
globalThis.MessageChannel =
MockMessageChannel as unknown as typeof MessageChannel;
}
With this solved, I'm seeing @react-email/render/dist/node/index.mjs attempting to run reactDOMServer.renderToReadableStream, when the actual import is reactDOMServer.default.renderToReadableStream. I don't know if this is just a bundling issue for react-email or something deeper, but patching fixes it.
With this solved, I'm seeing
@react-email/render/dist/node/index.mjsattempting to runreactDOMServer.renderToReadableStream, when the actual import isreactDOMServer.default.renderToReadableStream. I don't know if this is just a bundling issue for react-email or something deeper, but patching fixes it.
#2221 fixes this
Still not working even after resends merge, using convex and react-email
Yeah this issue is now pending this PR (which is indefinitely in draft) as I understand it: https://github.com/resend/react-email/pull/2225
@JClackett in case you're blocked, there's a workaround for Convex: https://discord.com/channels/1019350475847499849/1328374779635302510/1385305749705457846
@rakibatomnixima did you manage to fix this issue? I'm currently facing the exact same problem with the exact same project deps upgrade.
@rakibatomnixima did you manage to fix this issue? I'm currently facing the exact same problem with the exact same project deps upgrade.
Hey! I wasn’t able to fix the issue directly, but I did manage to get emails working reliably by exporting to static HTML and doing simple string replacements.
Here’s how I handled it:
1. Added raw-loader to Next.js webpack config to import .html files:
// next.config.ts
webpack: (config) => {
config.module.rules.push({
test: /\.html$/,
use: ['raw-loader'],
});
return config;
};
2. Used string {{placeholders}} inside the templates and then exported them to HTML
<Text>Hi {userFirstname || '{{userFirstname}}'},</Text>
<Button href={verificationLink || '{{verificationLink}}'}>Verify</Button>
3. Wrote a renderEmail function that loads the HTML and replaces placeholders dynamically:
import emailVerify from '../out/email/verify.html';
import { EmailVerificationEmailProps } from '../emails/email/verify';
const templatesLocationMap = {
EmailVerification: emailVerify,
// ... other templates
};
export type RenderEmailProps =
| {
template: 'EmailVerification';
emailProps: EmailVerificationEmailProps;
}
// ... other templates
export async function renderEmail({ template, emailProps }: RenderEmailProps): Promise<string> {
const html = templatesLocationMap[template];
return html.replace(/{{(.*?)}}/g, (match: string, p1: keyof RenderEmailProps['emailProps']) => emailProps[p1]);
}
4. Used the renderEmail function in my actual sendEmail function like this (using SES, but should work with others too):
type SendEmailProps = {
email: string;
subject: string;
renderEmailProps: RenderEmailProps;
};
export async function sendEmail({ email, subject, renderEmailProps }: SendEmailProps): Promise<{ success: boolean; message: string }> {
try {
const content = await renderEmail(renderEmailProps);
const params: SendEmailCommandInput = {
Source: `${appName} Team <${transactionalSource}>`,
Destination: {
ToAddresses: [email],
},
Message: {
Body: { Html: { Charset: 'UTF-8', Data: content } },
Subject: { Charset: 'UTF-8', Data: subject },
},
};
const response = await ses.sendEmail(params);
if (!response.MessageId) {
throw new Error('Failed to send email.');
}
return { success: true, message: 'Email sent successfully.' };
} catch (error) {
console.error('Error sending email:', error);
return { success: false, message: 'Failed to send email.' };
}
}
This way, you can still build and preview your emails using react-email, but bypass runtime rendering issues.
Hope this helps!
Thanks, pal @rakibatomnixima! Much appreciated!
Think this might well be resolved now in Cloudflare Workers as the MessageChannel API is now available:
https://x.com/_ashleypeacock/status/1955647915663282532
For reference, a solution that worked for me (when building with esbuild, I would assume there are comparable options for other tools) was to use this in the configuration:
alias: { 'react-dom/server.browser': 'react-dom/server.edge' },
Should be partially fixed in @react-email/[email protected]/@react-email/[email protected], but another issue happens when running it in Next.js on the edge, although it's another error.
If anyone still experiences this, let me know with a reproduction and I'll reopen.
@gabrielmfern some folks here using the Convex runtime are still experiencing this - would you be open to a pr adding a convex exports entry?
@erquhart Yes, would love if you could open it
But I wonder why Convex doesn't use the edge export itself?