Emmanuel Akinfulubi

Errors

Every error in this catalogue is produced by running the code that causes it, against the vendored Next.js, and the page shows what came back.

19 entries, each a real run. The messages the panel expected were written down before the run, and the build refuses to write this page if one of them did not appear. Where a mistake is a build error, the panel shows the block of the log that belongs to it. The lessons link here when they meet an error.

One common error is missing: a hydration mismatch, when the HTML the server sent differs from what the browser renders. It can only be seen in a browser, and nothing in this course opens one, so it was not run for Next.js. The React course ran one in a browser: hydration mismatch in the React catalogue.

  1. 1. A server-only module imported from a client component
  2. 2. A hook in a server component
  3. 3. metadata exported from a client file
  4. 4. A value exported from a "use server" file
  5. 5. "use cache" without the cacheComponents flag
  6. 6. middleware.js and proxy.js in the same project
  7. 7. Reading cookies outside Suspense with Cache Components
  8. 8. Route segment config that Cache Components rejects
  9. 9. cookies() used without await
  10. 10. params read without await
  11. 11. A function passed from a server component
  12. 12. window in a server component
  13. 13. next/image without a width
  14. 14. dynamic = 'error' on a page that reads cookies
  15. 15. An invalid revalidate value
  16. 16. next/router in the App Router
  17. 17. A default export in a route handler
  18. 18. A Server Action id the server does not know
  19. 19. A server variable read in a client component

Builds that fail

next build exits with status 1 and nothing is served. Five of these entries are separate mistakes in one project, found by a single build, so the panels show only the block of the log that belongs to each. The other three are separate projects. The lines that start with at are left out of these extracts.

First, the whole picture of the five-mistake build: how many errors it counted.

How the one build ended

fails on purpose · next build · Next 16.3.1

What next build printed (Next 16.3.1), then the checks, when the build ran it

next build

> Build error occurred
Error: Turbopack build failed with 7 errors:

ok   the log shows "Error: Turbopack build failed with 7 errors:"
ok   the log shows "> Build error occurred"

The build printed more than is shown. This panel shows the part of its log that holds > Build error occurred, without its stack-frame lines (the ones that start with at). The checks below it were made against the whole log.

Five mistakes were made, and the build counted seven errors, because two of the mistakes were reported more than once. The order the errors are listed in is Turbopack's own, so each entry below shows its own block.

1 A server-only module imported from a client component

next build fails

The mistake. secret.js begins with import 'server-only'. Panel.js, a client component, imports it, and a server page renders the panel.

The error block for the server-only import

fails on purpose · next build · Next 16.3.1

app/server-only/secret.js

import 'server-only';
export const key = 'sk-not-for-browsers';

app/server-only/Panel.js

'use client';
import { key } from './secret.js';
export default function Panel() {
  return <p>{key}</p>;
}

app/server-only/page.js

import Panel from './Panel.js';
export default function Page() {
  return <Panel />;
}

What next build printed (Next 16.3.1), then the checks, when the build ran it

next build

./app/server-only/secret.js:1:1
Error: You're importing a module that depends on "server-only" into a React Client Component module. This API is only available in Server Components but one of its parents is marked with "use client", so this module is also a Client Component.
    Learn more: https://nextjs.org/docs/app/building-your-application/rendering
> 1 | import 'server-only';
    | ^^^^^^^^^^^^^^^^^^^^^
  2 | export const key = 'sk-not-for-browsers';
  3 |

Ecmascript file had an error

Import traces:
  Client Component Browser:
    ./app/server-only/secret.js [Client Component Browser]
    ./app/server-only/Panel.js [Client Component Browser]
    ./app/server-only/Panel.js [Server Component]
    ./app/server-only/page.js [Server Component]

  Client Component SSR:
    ./app/server-only/secret.js [Client Component SSR]
    ./app/server-only/Panel.js [Client Component SSR]
    ./app/server-only/Panel.js [Server Component]
    ./app/server-only/page.js [Server Component]

ok   the log shows "You're importing a module that depends on \"server-only\" into a React Client Component module"
ok   the log shows "'server-only' cannot be imported from a Client Component module"

The build printed more than is shown. This panel shows the part of its log that holds You're importing a module that depends on "server-only", without its stack-frame lines (the ones that start with at). The checks below it were made against the whole log.

What it means. The server-only import is a marker. Its job is to make the build fail if the module could ever be sent to a browser, and a client component's imports are sent to the browser. The log held two messages for this one mistake (the panel's checks found both) and an import trace that walks from the page to the module.

The fix. Keep the code that uses the secret on the server: import it in a server component, and give the client component only the result, as a prop (lesson 10).

2 A hook in a server component

next build fails

The mistake. page.js has no 'use client' directive and calls useState.

The error block for useState in a server component

fails on purpose · next build · Next 16.3.1

app/hook/page.js

import { useState } from 'react';
export default function Page() {
  const [count] = useState(0);
  return <p>{count}</p>;
}

What next build printed (Next 16.3.1), then the checks, when the build ran it

next build

./app/hook/page.js:1:10
Error: You're importing a module that depends on `useState` into a React Server Component module. This API is only available in Client Components. To fix, mark the file (or its parent) with the `"use client"` directive.
    Learn more: https://nextjs.org/docs/app/api-reference/directives/use-client
> 1 | import { useState } from 'react';
    |          ^^^^^^^^
  2 | export default function Page() {
  3 |   const [count] = useState(0);
  4 |   return <p>{count}</p>;

Ecmascript file had an error

ok   the log shows "You're importing a module that depends on `useState` into a React Server Component module"

The build printed more than is shown. This panel shows the part of its log that holds You're importing a module that depends on `useState`, without its stack-frame lines (the ones that start with at). The checks below it were made against the whole log.

What it means. Components are server components unless a file or one of its parents is marked, and a hook such as useState works only in a client component, as the message says. The message points at the import, not at the call.

The fix. Put the state in a small component in a file that begins with 'use client' and render it from the server page (lesson 9).

3 metadata exported from a client file

next build fails

The mistake. page.js begins with 'use client' and also exports metadata.

The error block for metadata in a client file

fails on purpose · next build · Next 16.3.1

app/client-metadata/page.js

'use client';
export const metadata = { title: 'Nope' };
export default function Page() {
  return <p>hello</p>;
}

What next build printed (Next 16.3.1), then the checks, when the build ran it

next build

./app/client-metadata/page.js:2:14
Error: You are attempting to export "metadata" from a component marked with "use client", which is disallowed. "metadata" must be resolved on the server before the page component is rendered. Keep your page as a Server Component and move Client Component logic to a separate file. Read more: https://nextjs.org/docs/app/api-reference/functions/generate-metadata#why-generatemetadata-is-server-component-only
  1 | 'use client';
> 2 | export const metadata = { title: 'Nope' };
    |              ^^^^^^^^
  3 | export default function Page() {
  4 |   return <p>hello</p>;
  5 | }

Ecmascript file had an error

Import traces:
  Client Component Browser:
    ./app/client-metadata/page.js [Client Component Browser]
    ./app/client-metadata/page.js [Server Component]

  Client Component SSR:
    ./app/client-metadata/page.js [Client Component SSR]
    ./app/client-metadata/page.js [Server Component]

ok   the log shows "You are attempting to export \"metadata\" from a component marked with \"use client\", which is disallowed."

The build printed more than is shown. This panel shows the part of its log that holds You are attempting to export "metadata" from a component marked with "use client", without its stack-frame lines (the ones that start with at). The checks below it were made against the whole log.

What it means. Metadata is turned into tags on the server before the page renders, so the file that exports it has to be a server file. The message says to keep the page a server component and move the client logic into a file of its own.

The fix. Remove the directive from page.js, move whatever needed the browser to a client component, and keep metadata (or generateMetadata) in the page (lesson 23).

4 A value exported from a "use server" file

next build fails

The mistake. actions.js begins with 'use server', exports an async function and also exports export const limit = 10.

The error block for the extra export

fails on purpose · next build · Next 16.3.1

app/not-async/actions.js

'use server';
export const limit = 10;
export async function save() {}

What next build printed (Next 16.3.1), then the checks, when the build ran it

next build

./app/not-async/actions.js:2:1
Error: Only async functions are allowed to be exported in a "use server" file.
  1 | 'use server';
> 2 | export const limit = 10;
    | ^^^^^^^^^^^^^^^^^^^^^^^^
  3 | export async function save() {}
  4 |

Ecmascript file had an error

Import trace:
  Server Component:
    ./app/not-async/actions.js
    ./app/not-async/page.js

ok   the log shows "Only async functions are allowed to be exported in a \"use server\" file."
ok   the log shows "Export save doesn't exist in target module"

The build printed more than is shown. This panel shows the part of its log that holds Only async functions are allowed to be exported in a "use server" file., without its stack-frame lines (the ones that start with at). The checks below it were made against the whole log.

What it means. Every export of a 'use server' file becomes an address a browser can post to, so each one has to be an async function. The same build also reported, for the page that imports save, that the export does not exist in the module; the panel's checks found that message in the log too.

The fix. Move the constant to another file and import it where it is needed; keep only async functions in the action file (lesson 17).

5 "use cache" without the cacheComponents flag

next build fails

The mistake. page.js has a function that begins with 'use cache', in a project whose next.config.mjs does not set cacheComponents.

The error block for "use cache" without the flag

fails on purpose · next build · Next 16.3.1

app/use-cache/page.js

async function getTitle() {
  'use cache';
  return 'cached';
}
export default async function Page() {
  return <h1>{await getTitle()}</h1>;
}

What next build printed (Next 16.3.1), then the checks, when the build ran it

next build

./app/use-cache/page.js:2:3
Error: To use "use cache", please enable the feature flag `cacheComponents` in your Next.js config.

    Read more: https://nextjs.org/docs/app/api-reference/directives/use-cache#usage
  1 | async function getTitle() {
> 2 |   'use cache';
    |   ^^^^^^^^^^^
  3 |   return 'cached';
  4 | }
  5 | export default async function Page() {

Ecmascript file had an error

ok   the log shows "To use \"use cache\", please enable the feature flag `cacheComponents` in your Next.js config."

The build printed more than is shown. This panel shows the part of its log that holds To use "use cache", please enable the feature flag, without its stack-frame lines (the ones that start with at). The checks below it were made against the whole log.

What it means. The directive belongs to Cache Components, which is off by default in this version, so the compiler refuses it instead of ignoring it.

The fix. Set cacheComponents: true in the configuration, knowing that it changes how the whole project is cached (lesson 16), or leave the directive out.

6 middleware.js and proxy.js in the same project

next build fails

The mistake. The project has both middleware.js, the old name, and proxy.js.

The build with both files

fails on purpose · next build · Next 16.3.1

proxy.js

import { NextResponse } from 'next/server';
export function proxy() {
  return NextResponse.next();
}

middleware.js

import { NextResponse } from 'next/server';
export function middleware() {
  return NextResponse.next();
}

What next build printed (Next 16.3.1), then the checks, when the build ran it

next build

▲ Next.js 16.3.1 (Turbopack)
✓ Running next.config.mjs took <t>


> Build error occurred
Error: Both middleware file "./middleware.js" and proxy file "./proxy.js" are detected. Please use "./proxy.js" only. Learn more: https://nextjs.org/docs/messages/middleware-to-proxy
    at ignore-listed frames

ok   the log shows "Both middleware file \"./middleware.js\" and proxy file \"./proxy.js\" are detected"

What it means. The proxy replaced the middleware in version 16 and a project may have one of them. The build stops at the first problem, before it compiles anything, so there is no code frame: the message names both files.

The fix. Keep proxy.js, rename the exported function to proxy, and delete middleware.js (lesson 20).

7 Reading cookies outside Suspense with Cache Components

next build fails

The mistake. With cacheComponents: true, the home page awaits cookies() directly, with no <Suspense> around it.

The error from prerendering the page

fails on purpose · next build · Next 16.3.1

next.config.mjs

/** @type {import('next').NextConfig} */
const nextConfig = {
  cacheComponents: true,
};
export default nextConfig;

app/page.js

import { cookies } from 'next/headers';
export default async function Home() {
  const jar = await cookies();
  return <h1>Theme: {jar.get('theme')?.value ?? 'none'}</h1>;
}

What next build printed (Next 16.3.1), then the checks, when the build ran it

next build

Error: Route "/": Next.js encountered uncached or runtime data during prerendering.

`fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()` accessed outside of `<Suspense>` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.

Ways to fix this:
  - [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access
  - [cache] For uncached data (`fetch`, database calls): cache the access with `"use cache"` (does not apply to `connection()`)
  - [block] Set `export const instant = false` to allow a blocking route

Learn more: https://nextjs.org/docs/messages/blocking-prerender-dynamic
To get a more detailed stack trace and pinpoint the issue, try one of the following:
  - Start the app in development mode by running `next dev`, then open "/" in your browser to investigate the error.
  - Rerun the production build with `next build --debug-prerender` to generate better stack traces.
Error occurred prerendering page "/". Read more: https://nextjs.org/docs/messages/prerender-error
Export encountered an error on /page: /, exiting the build.
⨯ Next.js build worker exited with code: 1 and signal: null

ok   the log shows "Route \"/\": Next.js encountered uncached or runtime data during prerendering."
ok   the log shows "[stream] Provide a placeholder with `<Suspense fallback={...}>`"
ok   the log shows "[cache] For uncached data"
ok   the log shows "[block] Set `export const instant = false`"
ok   the log shows "Error occurred prerendering page \"/\"."

The build printed more than is shown. This panel shows the part of its log that holds Error: Route "/": Next.js encountered uncached or runtime data during prerendering., without its stack-frame lines (the ones that start with at). The checks below it were made against the whole log.

What it means. This error does not come from the compiler. The code compiled, and then the build rendered the page and found that it needed data that only exists per request. It names the route, lists the three ways out, and ends with the line that names the page it was prerendering. The stack frames were left out of the panel.

The fix. Pick one of the three the message lists: wrap the part that reads the cookie in <Suspense> with a fallback so the rest is prerendered; cache the access with "use cache"; or allow a blocking route. Streaming is the one lesson 12 and lesson 16 describe.

8 Route segment config that Cache Components rejects

next build fails

The mistake. With cacheComponents: true, page.js exports dynamic = 'force-dynamic'.

The error block for the dynamic export

fails on purpose · next build · Next 16.3.1

app/page.js

export const dynamic = 'force-dynamic';
export default function Home() {
  return <h1>Home</h1>;
}

What next build printed (Next 16.3.1), then the checks, when the build ran it

next build

./app/page.js:1:14
Error: Route segment config "dynamic" is not compatible with `nextConfig.cacheComponents`. Please remove it.
> 1 | export const dynamic = 'force-dynamic';
    |              ^^^^^^^
  2 | export default function Home() {
  3 |   return <h1>Home</h1>;
  4 | }

Ecmascript file had an error

ok   the log shows "Route segment config \"dynamic\" is not compatible with `nextConfig.cacheComponents`. Please remove it."

The build printed more than is shown. This panel shows the part of its log that holds Route segment config "dynamic" is not compatible, without its stack-frame lines (the ones that start with at). The checks below it were made against the whole log.

What it means. Under Cache Components the old route segment options that decided static against dynamic are gone: what is dynamic is decided by what the code reads. The message asks you to remove the export. (The same build is the one lesson 29 reads from top to bottom.)

The fix. Delete the export. To make part of a page per-request, read the request data inside <Suspense>; to keep a part static, cache it.

Pages that fail, in next dev

One route per mistake, requested in one next dev session. The panel shows the answer's status and what the development server printed for that request, as printed, including the code frame with the line marked. Request lines are the server's, with the durations masked.

9 cookies() used without await

next dev

The mistake. The page calls cookies().get('theme') and does not await cookies().

GET /err/cookies-sync

ran at build time · next dev · Next 16.3.1

app/err/cookies-sync/page.js

import { cookies } from 'next/headers';
export default function Page() {
  const theme = cookies().get('theme');
  return <p>{theme ? theme.value : 'no theme'}</p>;
}

cookies.mjs

const res = await fetch(process.env.BASE_URL + '/err/cookies-sync');
console.log('GET /err/cookies-sync ->', res.status);
check('the page answers 500', res.status, 500);

What the driver printed (Next 16.3.1, next dev), when the build ran it

node cookies.mjs

GET /err/cookies-sync -> 500
ok   the page answers 500

What next dev printed meanwhile

Error: Route "/err/cookies-sync" used `cookies().get`. `cookies()` returns a Promise and must be unwrapped with `await` or `React.use()` before accessing its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis
    at Page (app/err/cookies-sync/page.js:3:24)
  1 | import { cookies } from 'next/headers';
  2 | export default function Page() {
> 3 |   const theme = cookies().get('theme');
    |                        ^
  4 |   return <p>{theme ? theme.value : 'no theme'}</p>;
  5 | }
  6 |
⨯ TypeError: {imported module ./nodemodules/next/headers.js}.cookies(...).get is not a function
    at Page (app/err/cookies-sync/page.js:3:27)
  1 | import { cookies } from 'next/headers';
  2 | export default function Page() {
> 3 |   const theme = cookies().get('theme');
    |                           ^
  4 |   return <p>{theme ? theme.value : 'no theme'}</p>;
  5 | }
  6 | {
  digest: '<digest>'
}
 GET /err/cookies-sync 500 in <t> (next.js: <t>, application-code: <t>)

The driver ran against next dev -H 127.0.0.1 -p 0 of project dev; BASE_URL is the address Next printed.

What it means. In version 16 cookies(), headers() and the other request functions return Promises. The first line of the log is Next.js recognising the mistake and saying what to do; the second, the TypeError, is what JavaScript then does with it, and that one is what the page answers 500 for.

The fix. const jar = await cookies(), and make the component async (lesson 21).

10 params read without await

next dev

The mistake. The page reads params.slug straight from its props.

GET /err/params-sync/abc

ran at build time · next dev · Next 16.3.1

app/err/params-sync/[slug]/page.js

export default function Page({ params }) {
  return <p>{`slug: ${params.slug}`}</p>;
}

params.mjs

const res = await fetch(process.env.BASE_URL + '/err/params-sync/abc');
const text = />(slug: [^<]*)</.exec(await res.text())[1];
console.log('GET /err/params-sync/abc ->', res.status, ' the page says:', text);
check('the page answers 200', res.status, 200);
check('but where the slug should be it says undefined', text, 'slug: undefined');

What the driver printed (Next 16.3.1, next dev), when the build ran it

node params.mjs

GET /err/params-sync/abc -> 200  the page says: slug: undefined
ok   the page answers 200
ok   but where the slug should be it says undefined

What next dev printed meanwhile

Error: Route "/err/params-sync/[slug]" used `params.slug`. `params` is a Promise and must be unwrapped with `await` or `React.use()` before accessing its properties. Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis
    at Page (app/err/params-sync/[slug]/page.js:2:30)
  1 | export default function Page({ params }) {
> 2 |   return <p>{`slug: ${params.slug}`}</p>;
    |                              ^
  3 | }
  4 |
 GET /err/params-sync/abc 200 in <t> (next.js: <t>, application-code: <t>)

The driver ran against next dev -H 127.0.0.1 -p 0 of project dev; BASE_URL is the address Next printed.

What it means. This one is the quietest of the set: the page answered 200 and rendered slug: undefined, because a Promise has no property called slug. The log is the only sign. It says that the Promise must be unwrapped before its properties are read.

The fix. const { slug } = await params in an async component (lesson 5).

11 A function passed from a server component

next dev

The mistake. Two pages: one gives a <button> an onClick in a server component; the other passes a function prop, onPick, to a client component.

GET /err/handler and /err/fn-prop

ran at build time · next dev · Next 16.3.1

app/err/handler/page.js

export default function Page() {
  return <button onClick={() => console.log('clicked')}>Click</button>;
}

app/err/fn-prop/Picker.js

'use client';
export default function Picker({ onPick }) {
  return <button onClick={() => onPick('a')}>Pick</button>;
}

app/err/fn-prop/page.js

import Picker from './Picker.js';
export default function Page() {
  return <Picker onPick={(value) => console.log(value)} />;
}

handlers.mjs

const base = process.env.BASE_URL;
const handler = await fetch(base + '/err/handler');
const prop = await fetch(base + '/err/fn-prop');
console.log('GET /err/handler ->', handler.status);
console.log('GET /err/fn-prop ->', prop.status);
check('both pages answer 500', [handler.status, prop.status], [500, 500]);

What the driver printed (Next 16.3.1, next dev), when the build ran it

node handlers.mjs

GET /err/handler -> 500
GET /err/fn-prop -> 500
ok   both pages answer 500

What next dev printed meanwhile

⨯ Error: Event handlers cannot be passed to Client Component props.
  <button onClick={function onClick} children=...>
                  ^^^^^^^^^^^^^^^^^^
If you need interactivity, consider converting part of this to a Client Component.
    at ignore-listed frames {
  digest: '<digest>'
}
 GET /err/handler 500 in <t> (next.js: <t>, application-code: <t>)
⨯ Error: Event handlers cannot be passed to Client Component props.
  <... onPick={function onPick}>
              ^^^^^^^^^^^^^^^^^
If you need interactivity, consider converting part of this to a Client Component.
    at ignore-listed frames {
  digest: '<digest>'
}
 GET /err/fn-prop 500 in <t> (next.js: <t>, application-code: <t>)

The driver ran against next dev -H 127.0.0.1 -p 0 of project dev; BASE_URL is the address Next printed.

What it means. Props that cross from a server component to a client component are serialised, and a function cannot be. The message marks the offending prop with carets. Both variants gave the same message in this version.

The fix. Move the element that needs the handler into a client component and render that from the server (lesson 9), or pass a Server Action, which can cross (lesson 17).

12 window in a server component

next dev

The mistake. The page renders window.innerWidth.

GET /err/window

ran at build time · next dev · Next 16.3.1

app/err/window/page.js

export default function Page() {
  return <p>Width: {window.innerWidth}</p>;
}

window.mjs

const res = await fetch(process.env.BASE_URL + '/err/window');
console.log('GET /err/window ->', res.status);
check('the page answers 500', res.status, 500);

What the driver printed (Next 16.3.1, next dev), when the build ran it

node window.mjs

GET /err/window -> 500
ok   the page answers 500

What next dev printed meanwhile

⨯ ReferenceError: window is not defined
    at Page (app/err/window/page.js:2:21)
  1 | export default function Page() {
> 2 |   return <p>Width: {window.innerWidth}</p>;
    |                     ^
  3 | }
  4 | {
  digest: '<digest>'
}
 GET /err/window 500 in <t> (next.js: <t>, application-code: <t>)

The driver ran against next dev -H 127.0.0.1 -p 0 of project dev; BASE_URL is the address Next printed.

What it means. A server component runs in Node.js, which has no window, document or localStorage. The log shows the line.

The fix. Do browser-only work in a client component, inside an effect, which runs only in the browser (React course).

13 next/image without a width

next dev

The mistake. The page renders <Image src="/x.png" alt="" /> with no width and height.

GET /err/image

ran at build time · next dev · Next 16.3.1

app/err/image/page.js

import Image from 'next/image';
export default function Page() {
  return <Image src="/x.png" alt="" />;
}

image.mjs

const res = await fetch(process.env.BASE_URL + '/err/image');
console.log('GET /err/image ->', res.status);
check('the page answers 500', res.status, 500);

What the driver printed (Next 16.3.1, next dev), when the build ran it

node image.mjs

GET /err/image -> 500
ok   the page answers 500

What next dev printed meanwhile

⨯ Error: Image with src "/x.png" is missing required "width" property.
    at ignore-listed frames {
  digest: '<digest>'
}
 GET /err/image 500 in <t> (next.js: <t>, application-code: <t>)

The driver ran against next dev -H 127.0.0.1 -p 0 of project dev; BASE_URL is the address Next printed.

What it means. The component needs a size to reserve space before the picture loads (lesson 24), and an image from public/ gives it nothing to read one from.

The fix. Give width and height, or fill inside a sized parent, or import the file so its size is known.

14 dynamic = 'error' on a page that reads cookies

next dev

The mistake. The page exports dynamic = 'error', which asks Next.js to fail if the page cannot be static, and then awaits cookies().

GET /err/static-error

ran at build time · next dev · Next 16.3.1

app/err/static-error/page.js

import { cookies } from 'next/headers';
export const dynamic = 'error';
export default async function Page() {
  const jar = await cookies();
  return <p>{jar.size} cookies</p>;
}

static-error.mjs

const res = await fetch(process.env.BASE_URL + '/err/static-error');
console.log('GET /err/static-error ->', res.status);
check('the page answers 500', res.status, 500);

What the driver printed (Next 16.3.1, next dev), when the build ran it

node static-error.mjs

GET /err/static-error -> 500
ok   the page answers 500

What next dev printed meanwhile

⨯ Error: Route /err/static-error with `dynamic = "error"` couldn't be rendered statically because it used `cookies()`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering
    at Page (app/err/static-error/page.js:4:28)
  2 | export const dynamic = 'error';
  3 | export default async function Page() {
> 4 |   const jar = await cookies();
    |                            ^
  5 |   return <p>{jar.size} cookies</p>;
  6 | }
  7 | {
  code: 'NEXT_STATIC_GEN_BAILOUT',
  digest: '<digest>'
}
 GET /err/static-error 500 in <t> (next.js: <t>, application-code: <t>)

The driver ran against next dev -H 127.0.0.1 -p 0 of project dev; BASE_URL is the address Next printed.

What it means. The option is a promise that a page is static, and reading the request breaks it. The error carries a code, NEXT_STATIC_GEN_BAILOUT, which is how the framework tells this kind of failure from others.

The fix. Either remove the export and accept a per-request page, or take the cookie reading out of the page (lesson 11).

15 An invalid revalidate value

next dev

The mistake. The page exports revalidate = 'soon'.

GET /err/revalidate

ran at build time · next dev · Next 16.3.1

app/err/revalidate/page.js

export const revalidate = 'soon';
export default function Page() {
  return <p>revalidate</p>;
}

revalidate.mjs

const res = await fetch(process.env.BASE_URL + '/err/revalidate');
console.log('GET /err/revalidate ->', res.status);
check('the page answers 500', res.status, 500);

What the driver printed (Next 16.3.1, next dev), when the build ran it

node revalidate.mjs

GET /err/revalidate -> 500
ok   the page answers 500

What next dev printed meanwhile

⨯ Error: Invalid revalidate value "soon" on "/err/revalidate", must be a non-negative number or false
    at ignore-listed frames
⨯ Error: Invalid revalidate value "soon" on "/err/revalidate", must be a non-negative number or false
    at ignore-listed frames {
  page: '/err/revalidate'
}
 GET /err/revalidate 500 in <t> (next.js: <t>, application-code: <t>)

The driver ran against next dev -H 127.0.0.1 -p 0 of project dev; BASE_URL is the address Next printed.

What it means. The option takes a number of seconds or false. A string is rejected when the route is read, and here the message was printed twice.

The fix. Use a number: export const revalidate = 60 (lesson 14).

16 next/router in the App Router

next dev

The mistake. A client component imports useRouter from next/router, the Pages Router's.

GET /err/old-router

ran at build time · next dev · Next 16.3.1

app/err/old-router/Go.js

'use client';
import { useRouter } from 'next/router';
export default function Go() {
  const router = useRouter();
  return <button onClick={() => router.push('/')}>Go</button>;
}

app/err/old-router/page.js

import Go from './Go.js';
export default function Page() {
  return <Go />;
}

old-router.mjs

const res = await fetch(process.env.BASE_URL + '/err/old-router');
console.log('GET /err/old-router ->', res.status);
check('the page answers 500', res.status, 500);

What the driver printed (Next 16.3.1, next dev), when the build ran it

node old-router.mjs

GET /err/old-router -> 500
ok   the page answers 500

What next dev printed meanwhile

⨯ Error: NextRouter was not mounted. https://nextjs.org/docs/messages/next-router-not-mounted
    at Go (app/err/old-router/Go.js:4:27)
  2 | import { useRouter } from 'next/router';
  3 | export default function Go() {
> 4 |   const router = useRouter();
    |                           ^
  5 |   return <button onClick={() => router.push('/')}>Go</button>;
  6 | }
  7 | {
  digest: '<digest>'
}
 GET /err/old-router 500 in <t> (next.js: <t>, application-code: <t>)

The driver ran against next dev -H 127.0.0.1 -p 0 of project dev; BASE_URL is the address Next printed.

What it means. The two routers have separate hooks. In next dev the import resolves, and the error comes when the component renders and calls the hook with no Pages Router to talk to.

The fix. Import useRouter from next/navigation (lesson 6).

17 A default export in a route handler

next dev

The mistake. route.js has export default function handler(), as a Pages Router API route would.

GET /err/default-export

ran at build time · next dev · Next 16.3.1

app/err/default-export/route.js

export default function handler() {
  return new Response('hello');
}

default-export.mjs

const res = await fetch(process.env.BASE_URL + '/err/default-export');
console.log('GET /err/default-export ->', res.status);
check('the page answers 405', res.status, 405);

What the driver printed (Next 16.3.1, next dev), when the build ran it

node default-export.mjs

GET /err/default-export -> 405
ok   the page answers 405

What next dev printed meanwhile

⨯ Detected default export in '[project]/app/err/default-export/route.js'. Export a named export for each HTTP method instead.
⨯ No HTTP methods exported in '[project]/app/err/default-export/route.js'. Export a named export for each HTTP method.
 GET /err/default-export 405 in <t> (next.js: <t>, application-code: <t>)

The driver ran against next dev -H 127.0.0.1 -p 0 of project dev; BASE_URL is the address Next printed.

What it means. A route handler exports one function for each HTTP method it answers. A default export is ignored, so the file exports no method at all, and the request gets a 405. Both messages are in the log.

The fix. Name the export after the method: export function GET() (lesson 19).

Found while serving

Two that next build accepts and next start serves, one with an error and one with none.

18 A Server Action id the server does not know

next start

The mistake. A POST names an action id that this build never made: a stale form from an older deployment, or a made-up one.

POST with an unknown action id

ran at build time · next build + next start · Next 16.3.1

unknown-action.mjs

const base = process.env.BASE_URL;
const body = new FormData();
body.append('$ACTION_ID_' + '0'.repeat(40), '');
const res = await fetch(base + '/actions', { method: 'POST', body, headers: { origin: base } });
console.log('POST with an unknown action id ->', res.status);
check('the server answers 500', res.status, 500);

What the driver printed (Next 16.3.1, next build + next start), when the build ran it

node unknown-action.mjs

POST with an unknown action id -> 500
ok   the server answers 500

What next start printed meanwhile

⨯ Error: Failed to find Server Action. This request might be from an older or newer deployment.
Read more: https://nextjs.org/docs/messages/failed-to-find-server-action
    at ignore-listed frames
⨯ Error: Failed to find Server Action. This request might be from an older or newer deployment.
Read more: https://nextjs.org/docs/messages/failed-to-find-server-action
    at ignore-listed frames

The driver ran against next start -H 127.0.0.1 -p 0 of project app, right after its next build; BASE_URL is the address Next printed.

What it means. Action ids are made by the build, so a form rendered by one build can be posted after the next one is deployed. The message names that cause. An id the server has never had is treated the same way, which is also why guessing ids gets an attacker nothing (and why actions still check the session themselves, lesson 27).

The fix. For users with an old page open, a reload is enough; teams that deploy often keep the old build's actions available for a while. In your own code there is nothing to change.

19 A server variable read in a client component

next start

The mistake. Leaky.js is a client component and renders process.env.APP_GREETING, a variable without the NEXT_PUBLIC_ prefix.

The same component, on the server and in the scripts sent to browsers

ran at build time · next build + next start · Next 16.3.1

app/env/leak/Leaky.js

'use client';

export default function Leaky() {
  // APP_GREETING has no NEXT_PUBLIC_ prefix, so nothing replaces this expression in the code sent to browsers.
  return <p data-leaky>{`APP_GREETING is ${String(process.env.APP_GREETING)}`}</p>;
}

app/env/leak/page.js

import Leaky from './Leaky.js';

export default function Page() {
  return (
    <main>
      <h1>A server variable in a client component</h1>
      <Leaky />
    </main>
  );
}

leak.mjs

import { readdirSync, readFileSync } from 'node:fs';

const base = process.env.BASE_URL;
const html = await (await fetch(base + '/env/leak')).text();
const rendered = /<p data-leaky[^>]*>([^<]*)<\/p>/.exec(html)[1];
const scripts = readdirSync(process.env.APP_DIR + '/.next/static/chunks', { withFileTypes: true })
  .filter((entry) => entry.isFile() && entry.name.endsWith('.js'))
  .map((entry) => readFileSync(entry.parentPath + '/' + entry.name, 'utf8'));
const mentionsName = scripts.some((text) => text.includes('APP_GREETING'));
const holdsValue = scripts.some((text) => text.includes('hello from .env'));

console.log('the server rendered:', rendered);
console.log('a script sent to browsers mentions the variable:', mentionsName);
console.log('a script sent to browsers holds its value:', holdsValue);
check('the server rendered the value, because the server has the real environment', rendered, 'APP_GREETING is hello from .env');
check('the scripts sent to browsers refer to the variable', mentionsName, true);
check('and none of them holds its value', holdsValue, false);

What the driver printed (Next 16.3.1, next build + next start), when the build ran it

node leak.mjs

the server rendered: APP_GREETING is hello from .env
a script sent to browsers mentions the variable: true
a script sent to browsers holds its value: false
ok   the server rendered the value, because the server has the real environment
ok   the scripts sent to browsers refer to the variable
ok   and none of them holds its value

The driver ran against next start -H 127.0.0.1 -p 0 of project app, right after its next build; BASE_URL is the address Next printed.

What it means. There is no error anywhere, which is what makes this one worth knowing. The server renders the component too, with the real environment, so the HTML has the value. Only NEXT_PUBLIC_ variables are written into the code sent to the browser at build time (lesson 22). The panel shows both halves: the value in the HTML, and in no script, though a script does name the variable. What a browser then renders was not run, since no panel here has one. Because no script holds the value, the browser cannot render the text the server sent, and React would report that difference as a hydration mismatch.

The fix. Read the variable in a server component and pass it down as a prop, or, if it is meant to be public, rename it with the NEXT_PUBLIC_ prefix and build again.

Newsletter

New writing, when it lands.

Occasional notes on shipping software with AI agents. No schedule, no filler, unsubscribe in one click.