The Build That Panicked
Date: 2025-04-28 Duration: About an hour Issue: Astro build crashing on Cloudflare Pages Root Cause: Dependency drift from using "latest"
The Error
panic: html: bad parser state: originalIM was set twice [recovered]
panic: interface conversion: string is not error: missing method Error
That's not a JavaScript error. That's a Go panic. Something deep in the Astro compiler was dying.
The Wild Goose Chase
The error pointed vaguely at HTML parsing. So I went hunting through components.
Attempt 1: Fixed JSX-style comments to HTML comments.
<!-- Before -->
{/* <Fragment set:html={social.icon} /> */}
<!-- After -->
<!-- <Fragment set:html={social.icon} /> -->
Same error.
Attempt 2: Removed SVG icons from social links entirely.
Same error.
Attempt 3: Removed set:html directives from Terminal component.
Same error.
Attempt 4: Created a minimal test layout with almost no content.
Same. Error.
The parser was panicking regardless of what I changed. This wasn't a code problem.
The Real Clue
Buried in the build logs:
npm WARN EBADENGINE Unsupported engine {
package: 'yocto-spinner',
required: { node: 'newer runtime' },
current: { node: 'older runtime', npm: 'older npm' }
}
The build was running an older Node runtime against dependencies that expected a newer one.
But that was just a warning. The real culprit was in package.json:
"dependencies": {
"@astrojs/cloudflare": "latest",
"@astrojs/mdx": "latest",
"@astrojs/rss": "latest",
"@astrojs/sitemap": "latest",
"@astrojs/tailwind": "latest",
"astro": "latest"
}
Every single dependency set to latest.
Why "latest" Kills Builds
When you deploy to Cloudflare Pages, it runs npm install fresh. If your package.json says latest, npm pulls the newest package release available at that moment.
The problem:
- I pushed code that worked with the Astro release I had locally
- Two weeks later, Cloudflare ran the build
- npm installed a newer Astro release
- The newer release had a parser bug, or incompatibility, or breaking change
- Build panicked
The code hadn't changed. The dependencies had.
The Fix
Pinned every dependency to the known-good release set:
"dependencies": {
"@astrojs/cloudflare": "known-good",
"@astrojs/mdx": "known-good",
"@astrojs/rss": "known-good",
"@astrojs/sitemap": "known-good",
"@astrojs/tailwind": "known-good",
"astro": "known-good"
}
Cleared the Cloudflare Pages cache. Redeployed.
Build passed. Site worked.
The Secondary Fix
Also aligned the Node.js runtime in Cloudflare Pages settings:
Environment Variables:
NODE_VERSION = known-good-runtime
Now the build environment matched what the dependencies expected.
Why the Error Message Was Useless
The "originalIM was set twice" panic came from the Astro compiler's Go-based HTML parser. When the parser encountered code that worked on one release but not another, it didn't throw a helpful error; it panicked.
The parser didn't know which component caused the issue. It just knew its internal state was corrupted. So it pointed at "HTML parsing" generically.
I could have commented out every single component in my project and the error would have persisted — because the problem was the parser itself, not my code.
Lessons
Never use "latest" in production package.json. Pin dependencies to known-good releases.
Check npm warnings for runtime mismatches. The "EBADENGINE" warning was a clue that dependencies were fighting each other.
Cryptic errors sometimes aren't about your code. When the error message doesn't match what you're changing, step back. The problem might be environmental.
Cloudflare Pages rebuilds from scratch. Every deploy gets fresh dependencies. If you're using floating dependency ranges, you're gambling on what gets installed.
The Prevention
Added a package-lock.json to the repo. Now npm installs the exact dependency set that worked locally.
Also added a .nvmrc file:
known-good-runtime
And documented the required Node runtime in the README.
Future me will thank present me.
The build panicked. I almost did too. Turns out "latest" is the enemy of reproducible builds.