How to Fix: Unable to run production build in next 15.2.0-canary.10, 15.2.0-canary.9 seems to work
Next.js 15.2.0-canary.10 can fail only in the production build phase even when 15.2.0-canary.9 works, which is the classic signal of a canary regression rather than an application bug. When a build breaks only after upgrading one canary version, the fastest path is to isolate the regression, pin the last known good version, clear build artifacts, and verify whether the failure is caused by SWC compilation, App Router server bundling, React Server Components, or a dependency being evaluated differently during optimization.
Table of Contents
Understanding the Root Cause
This issue pattern usually appears when a new canary introduces a change in the production compiler pipeline or server/client module graph analysis. In practice, that means development mode can still appear healthy because next dev and next build do not execute code in exactly the same way.
With Next.js canary releases, especially around major internal refactors, the production build may become stricter about one or more of the following:
- Static analysis of imports that were previously tolerated.
- Server-only versus client-only boundaries in the App Router.
- Tree shaking or module evaluation order changes.
- SWC transforms that expose invalid syntax patterns or unsupported package output.
- Build-time execution of code paths that are skipped in development.
The clue in this issue is that 15.2.0-canary.9 works while 15.2.0-canary.10 fails. That strongly suggests a regression introduced between those two releases. Your app may still contain a fragile pattern, but the immediate operational fix is to treat the newer canary as unstable and either pin the previous version or isolate the exact code path triggering the build failure.
Technically, the root cause is often one of these scenarios:
- A package is imported in a Server Component but relies on browser globals such as
window,document, ornavigator. - A file intended for the client is missing the
'use client'directive, so the compiler treats it as a server boundary during production optimization. - A dynamic import, environment variable, or side-effectful module is being evaluated during build-time prerendering.
- The new canary changed internal bundling behavior and exposed a dependency compatibility issue that older canaries did not surface.
In other words, the production build is not randomly failing: canary.10 is likely exercising a stricter code path than canary.9.
Step-by-Step Solution
The most reliable fix is to stabilize first, then isolate the regression.
1. Pin the last known good version
If production is blocked, revert to the canary that still builds successfully.
npm install next@15.2.0-canary.9 react@latest react-dom@latest
Or with pnpm:
pnpm add next@15.2.0-canary.9
Or with yarn:
yarn add next@15.2.0-canary.9
Then remove stale artifacts and rebuild:
rm -rf .next node_modules package-lock.json pnpm-lock.yaml yarn.lock
npm install
npm run build
This confirms whether the issue is truly version-specific rather than caused by cached output.
2. Verify the regression with a clean diff
Test both versions back to back from a clean workspace.
rm -rf .next node_modules
npm install
npm install next@15.2.0-canary.9
npm run build
rm -rf .next node_modules
npm install
npm install next@15.2.0-canary.10
npm run build
If only canary.10 fails, you have a reproducible framework regression window.
3. Inspect the failing route or module
Production build failures typically identify the route, layout, page, or imported package. Focus on files that recently changed or that mix server and client behavior.
Look for these high-risk patterns:
// Problem: browser API in a Server Component
export default async function Page() {
const value = window.localStorage.getItem('token')
return <div>{value}</div>
}
Move browser-only logic into a client component:
'use client'
import { useEffect, useState } from 'react'
export default function TokenView() {
const [token, setToken] = useState('')
useEffect(() => {
setToken(window.localStorage.getItem('token') || '')
}, [])
return <div>{token}</div>
}
And use it from the server page safely:
import TokenView from './TokenView'
export default function Page() {
return <TokenView />
}
4. Audit 'use client' boundaries
A missing directive can become visible only after a canary upgrade.
// If this component uses hooks, event handlers, or browser APIs,
// it must start with 'use client'
'use client'
import { useState } from 'react'
export default function SearchBox() {
const [query, setQuery] = useState('')
return <input value={query} onChange={(e) => setQuery(e.target.value)} />
}
Any component using React hooks, event handlers, or browser globals should be explicitly marked as a client component.
5. Guard build-time execution
If a module reads runtime-only values during import, production optimization may crash before the app even starts.
// Problem: side effect at import time
const apiBase = window.location.origin
export async function getData() {
const res = await fetch(apiBase + '/api/data')
return res.json()
}
Use runtime-safe branching instead:
export async function getData() {
const apiBase = typeof window !== 'undefined' ? window.location.origin : process.env.NEXT_PUBLIC_SITE_URL
const res = await fetch(apiBase + '/api/data')
return res.json()
}
Even better, avoid depending on window in shared modules used by the server build.
6. Check dependency compatibility
Some libraries work in development but fail when the bundler optimizes them for production. If the stack trace points to a third-party package, try one of these strategies:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['problematic-package'],
}
module.exports = nextConfig
Or replace top-level imports with a client-only dynamic import when appropriate:
import dynamic from 'next/dynamic'
const HeavyBrowserWidget = dynamic(() => import('./HeavyBrowserWidget'), {
ssr: false,
})
export default function Page() {
return <HeavyBrowserWidget />
}
7. Generate a minimal reproduction
If the issue still reproduces only on canary.10, strip the app down until the failing route is isolated. Use the official reproduction template and copy only the smallest set of files needed to trigger the failure.
A strong reproduction usually includes:
- The exact Next.js version.
- The exact package manager and lockfile.
- The build command that fails.
- The full stack trace.
- The smallest route or component that triggers the error.
8. Lock production to stable or known-good canary
If you must ship immediately, do not keep production on the broken canary. Pin the version in package.json:
{
"dependencies": {
"next": "15.2.0-canary.9"
}
}
This prevents CI or teammate installs from floating to a broken version.
9. Report the regression with actionable diagnostics
When filing or updating the issue, include:
- A passing build on 15.2.0-canary.9.
- A failing build on 15.2.0-canary.10.
- The exact error output from
next build. - A minimal repo based on the reproduction template.
That gives the Next.js maintainers a narrow regression range and makes the bug dramatically easier to fix.
Common Edge Cases
- Build cache confusion: Old artifacts in
.nextor lockfile drift can make a framework regression look inconsistent. Always test after a full cleanup. - Environment-variable mismatches: Production build may require variables that are not needed in development. Missing values can trigger crashes during prerendering or route analysis.
- Static generation side effects: Code inside
generateMetadata,generateStaticParams, or server data loaders may run at build time and fail only duringnext build. - Mixed ESM/CJS dependencies: A canary update may tighten interop behavior, exposing packages with incomplete module exports.
- Node.js version drift: If local and CI use different Node versions, a canary regression may appear only in one environment. Confirm the exact runtime with
node -v. - Client package imported from server code: Libraries that depend on DOM APIs may build in one version and fail in another due to improved server graph validation.
FAQ
Why does next dev work while next build fails?
Development mode and production mode use different optimization and evaluation paths. Production performs stricter bundling, tree shaking, prerender analysis, and module execution, so invalid server/client boundaries or import-time side effects often surface only during next build.
Is downgrading from canary.10 to canary.9 a valid fix?
Yes. For a blocking regression, pinning the last known good canary is the correct short-term fix. It restores deployability while you isolate the minimal reproduction and wait for an upstream patch.
How do I know whether this is my app or a Next.js bug?
If the same codebase builds on 15.2.0-canary.9 and fails on 15.2.0-canary.10 after a clean install, that is strong evidence of a framework regression. If simplifying one route or dependency removes the failure, your app may still be triggering the bug through a specific edge case, but the version-to-version break is still useful evidence for maintainers.
The practical resolution is straightforward: pin canary.9, clear caches, isolate the failing route, audit server/client boundaries, and submit a minimal reproduction. That approach both unblocks production and produces the exact diagnostics needed to get the regression fixed upstream.