vercel/next.js · error · Error

Page with dynamic = "error" encountered dynamic data method

Error message

Page with dynamic = "error" encountered dynamic data method on ${path}.

What it means

Thrown in the App Router export path when a route declared `export const dynamic = 'error'` (a.k.a. the page opts out of dynamic rendering) yet, during prerender, the render result reports cacheControl.revalidate === 0 AND isDynamicError is true. The dynamic='error' contract promises the route is fully static; the framework detects that a dynamic data method (cookies(), headers(), draftMode(), searchParams sync access, request.time, etc.) was invoked and refuses to silently fall back to dynamic rendering.

Source

Thrown at packages/next/src/export/routes/app-page.ts:115

      flightData,
      cacheControl = { revalidate: false, expire: undefined },
      postponed,
      fetchTags,
      fetchMetrics,
      segmentData,
      prefetchHints,
      renderResumeDataCache,
      hasPendingUi,
    } = metadata

    // Ensure we don't postpone without having PPR enabled.
    if (postponed && !renderOpts.experimental.isRoutePPREnabled) {
      throw new Error('Invariant: page postponed without PPR being enabled')
    }

    if (cacheControl.revalidate === 0) {
      if (isDynamicError) {
        throw new Error(
          `Page with dynamic = "error" encountered dynamic data method on ${path}.`
        )
      }
      const { staticBailoutInfo = {} } = metadata

      if (debugOutput && staticBailoutInfo?.description) {
        logDynamicUsageWarning({
          path,
          description: staticBailoutInfo.description,
          stack: staticBailoutInfo.stack,
        })
      }

      return { cacheControl, fetchMetrics }
    }

    // If page data isn't available, it means that the page couldn't be rendered
    // properly so long as we don't have unknown route params. When a route doesn't

View on GitHub (pinned to 0ae8c72462)

Solutions

  1. Remove `export const dynamic = 'error'` from the route if it genuinely needs request data, or switch to `dynamic = 'force-dynamic'`.
  2. Push the dynamic access (cookies/headers/searchParams) into a child segment or Suspense boundary that is allowed to be dynamic, keeping the parent static.
  3. If the dynamic call is unintentional, locate it via the build's 'reason' log and replace it with a static equivalent (e.g. read from generateStaticParams params, not from cookies).
  4. Verify no imported library implicitly accesses request storage; wrap it behind a client component or a 'use cache' boundary.

Example fix

// before — app/page.tsx
import { cookies } from 'next/headers'
export const dynamic = 'error'
export default function Page() {
  const theme = cookies().get('theme')?.value
  return <p>{theme}</p>
}
// after — drop the conflicting contract
import { cookies } from 'next/headers'
export const dynamic = 'force-dynamic'
export default function Page() {
  const theme = cookies().get('theme')?.value
  return <p>{theme}</p>
}
Defensive patterns

Strategy: validation

Validate before calling

// Scan the route module's source for dynamic APIs before declaring dynamic='error'
const DYNAMIC_APIS = ['cookies(', 'headers(', 'draftMode(', 'searchParams', 'request\.']
function safeToUseDynamicError(source: string): boolean {
  return !DYNAMIC_APIS.some((re) => new RegExp(re).test(source))
}

Type guard

// runtime check: a page declaring dynamic='error' must not touch request storage
import { isCallerDynamicAccessSupported } from './checks'
function assertStaticContract(usesDynamic: boolean, declared: string) {
  if (usesDynamic && declared === 'error') {
    throw new Error('Route uses dynamic APIs but declares dynamic="error"')
  }
}

Try / catch

// Not catchable in user code — runs at build time.
// Instead, gate the contract with a CI grep:
//   rg -n "cookies\(|headers\(" app/**/page.tsx | xargs grep -l "dynamic.*error"

Prevention

When it happens

Trigger: An app-router page or layout exports `const dynamic = 'error'` and then calls cookies(), headers(), searchParams, or another dynamic request API during render or in a generateMetadata/generateStaticParams helper. Triggered at build time during prerender of that route.

Common situations: Developers copy a server component that reads cookies() for personalization but also leave `dynamic = 'error'` from a template. Or a third-party dependency (auth, A/B testing) reaches into request storage. Upgrading Next.js can newly flag a previously-tolerated access because the dynamic-usage detector tightened.

Related errors


AI-assisted analysis of vercel/next.js@0ae8c72462 (2026-08-06). Data as JSON: /api/errors/d62a9048c6e9b55d. Report an issue: GitHub.