diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index b2fe24163753..d83289310db8 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -1769,6 +1769,7 @@ export default async function loadConfig( configFileName = basename(path) let userConfigModule: any + let loadedConfig: NextConfig try { const envBefore = Object.assign({}, process.env) @@ -1809,6 +1810,18 @@ export default async function loadConfig( return userConfigModule } + + // `normalizeConfig` invokes the user's exported config function (or + // awaits its returned promise) if it is one. Errors thrown from that + // call belong to the same "failed to load config" category as parse + // errors from `import()` above, so we keep them inside this try/catch + // to attach the same framing message. + loadedConfig = Object.freeze( + (await normalizeConfig( + phase, + interopDefault(userConfigModule) + )) as NextConfig + ) } catch (err) { // Capture the error for MCP tool reporting NextInstanceErrorState.nextConfig.push(err) @@ -1820,13 +1833,6 @@ export default async function loadConfig( throw err } - const loadedConfig = Object.freeze( - (await normalizeConfig( - phase, - interopDefault(userConfigModule) - )) as NextConfig - ) - if (loadedConfig.experimental) { for (const name of Object.keys( loadedConfig.experimental diff --git a/test/production/config-evaluation-error/config-evaluation-error.test.ts b/test/production/config-evaluation-error/config-evaluation-error.test.ts new file mode 100644 index 000000000000..57a78e74d2cd --- /dev/null +++ b/test/production/config-evaluation-error/config-evaluation-error.test.ts @@ -0,0 +1,68 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('next.config evaluation error', () => { + describe('production mode', () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + if (skipped) return + + async function buildAndGetOutput(): Promise { + const start = next.cliOutput.length + await next.build() + return next.cliOutput.slice(start) + } + + it('should report a helpful error when the config function throws synchronously', async () => { + await next.patchFile( + 'next.config.js', + ` + module.exports = () => { + return { foo: new Uint8Array(5_000_000_000) } + } + ` + ) + const output = await buildAndGetOutput() + + expect(output).toContain('Invalid typed array length') + expect(output).toContain( + 'Failed to load next.config.js, see more info here https://nextjs.org/docs/messages/next-config-error' + ) + }) + + it('should report a helpful error when the config module throws at the top level', async () => { + await next.patchFile( + 'next.config.js', + ` + const buf = new Uint8Array(5_000_000_000) + module.exports = { foo: buf } + ` + ) + const output = await buildAndGetOutput() + + expect(output).toContain('Invalid typed array length') + expect(output).toContain( + 'Failed to load next.config.js, see more info here https://nextjs.org/docs/messages/next-config-error' + ) + }) + + it('should report a helpful error when the config function rejects', async () => { + await next.patchFile( + 'next.config.js', + ` + module.exports = async () => { + throw new Error('boom from async config plugin') + } + ` + ) + const output = await buildAndGetOutput() + + expect(output).toContain('boom from async config plugin') + expect(output).toContain( + 'Failed to load next.config.js, see more info here https://nextjs.org/docs/messages/next-config-error' + ) + }) + }) +}) diff --git a/test/production/config-evaluation-error/pages/index.js b/test/production/config-evaluation-error/pages/index.js new file mode 100644 index 000000000000..821fdd14d097 --- /dev/null +++ b/test/production/config-evaluation-error/pages/index.js @@ -0,0 +1,3 @@ +export default function Page(props) { + return

index page

+}