skip to content

Why does code that works against a development server fail only when the production build runs it?

level: middleimportance: must knowfreq 62%

answer

  1. the build runs your code
  2. only visited routes ran in development
  3. no browser, no request while prerendering
  4. the build machine is not yours

basics

~20 s

The build executes your code: it compiles every route and renders the prerenderable ones on the build machine, with no browser and no incoming request. Paths a development session never reached fail there for the first time.

solid answer

~50 s

Because the build runs your code, and runs it under conditions development never produced. Three families cover most of these failures. **Coverage**: a development server compiles and renders only the routes you actually opened, while the build touches every route it can render ahead of time — so a page nobody visited breaks for the first time in the build. **Context**: prerendering happens with no browser and no request, so a module that reaches for a browser global at import time, or expects a request-bound value such as a URL or a header, throws. **Environment**: the build runs on a build machine, so a file that exists only on your laptop, an absolute path, or a value that is present in your shell but absent where the build runs is suddenly missing. In all three the build is being stricter, not broken.

go deeper

for a junior

Hold on to one idea: the build runs your code for every route, not just the ones you opened. Most build-only errors are ordinary errors in pages you never visited.

for a middle

Separate the three families — coverage, context, environment — and be precise about browser globals: module scope and the render path break, post-hydration effects do not.

for a senior

Talk about placement and signal: build on every change so coverage failures land on the commit, validate required configuration at the start of the build, and keep the build log readable enough to diagnose from.

for a principal

Decide what the build is allowed to be — a gate that must stay fast, or the team's first real test run — and make the consequences of that choice explicit in the pipeline's design.

The sentence that explains this whole class of bug is short: **a production build is not a packaging step, it is an execution of your code.** It compiles every route, and for every route it is allowed to render ahead of time it actually renders it — running your data functions and your components in a server context on a build machine. Anything that only holds under the conditions a development session happened to create will be discovered there. ## Family one: coverage A development server compiles on demand. It touches a route when a request arrives for it and not before. Over an afternoon you might exercise five routes out of eighty. The build has no such luxury: it walks the whole route table. The practical effect is that many "build-only" failures are not build-specific at all — they are ordinary errors in code that development never ran. The obscure settings page, the route behind a feature flag nobody turned on, the error page itself: all of them are first executed by the build. - A syntax or type error in an unvisited module surfaces at build time. - A route whose data function throws for its default parameters surfaces at build time. - A route that renders fine for one set of parameters but not for another surfaces when the build enumerates the set. ## Family two: context Prerendering runs your components with **no browser and no request**. Two subfamilies follow. **Browser globals.** Code that touches the document, the window object, a storage API or anything else supplied by a browser has nothing to touch. Note the subtlety: touching such a global *inside* an effect that only runs after hydration is fine, because prerendering never runs it. The failures are the ones at module scope or in the render path itself — a library that inspects the document when it is imported is the classic example, and the reason meta-frameworks give you a way to exclude a module from server execution or to defer it to the browser. **Request-bound values.** Rendering during a build has no incoming request, so a full URL, a header, a cookie or a client address does not exist. Reading one is either an error or, worse, a value that gets baked into the prerendered output and is then wrong for every visitor. ## Family three: environment The build machine is not your machine. | Works locally because… | Fails in the build because… | |---|---| | A data file sits untracked in your working directory | The build machine only has what the repository carries | | A path is written absolutely for your home directory | That directory does not exist there | | A case-insensitive filesystem forgives a wrong import casing | The build machine's filesystem is case-sensitive | | A configuration value is exported in your shell | The build environment was never given it | | A dependency is installed but not recorded | Only recorded dependencies are installed | The configuration row deserves a caveat: whether a missing value fails the build, produces a broken artefact, or is simply read later at runtime depends on how it is consumed, and meta-frameworks differ in what they do here. The failure mode worth remembering is that **absence at build time is silent unless you make it loud** — validate the values a build genuinely requires at the start of the build rather than discovering the gap in a rendered page. ## Why the order feels backwards Developers experience this as the build being pickier than development, which is true but incomplete. The build is doing more: more routes, in a stricter context, on a cleaner machine. Development is not more permissive so much as less thorough. That framing suggests the fix. Rather than treating the build as a gate that occasionally rejects you, treat it as the **first honest test run** and schedule it accordingly: 1. Build in the pipeline on every change, so coverage failures arrive within minutes of the commit that caused them. 2. Build locally before anything risky ships, so the loop is yours rather than the pipeline's. 3. Validate required configuration explicitly at the start of the build so an absent value produces one clear message rather than a strange rendering error. 4. Keep browser-only work out of module scope and out of the render path, so prerendering has nothing to trip over. Do those four and this entire family of surprises collapses into a build log you read while you still remember the change.

  • A component reads a browser storage API and works in development. Where exactly does the build break?
    Only if that read happens while the component is being rendered, or at module scope when the module is imported — prerendering has no such API. The same read inside an effect that runs after hydration never executes during the build, which is why the identical line can be safe or fatal depending on where it sits.
  • Why can a route that renders correctly in development produce wrong content when prerendered?
    Because a build render has no request. Anything derived from the visitor — the full URL, a header, a cookie, a locale guess, the current time — is either unavailable or captured once at build time and then frozen into the stored document for every later visitor.

saying these in an interview costs you the question

  • Assumes the build only bundles files and never executes application code
  • Thinks a route that was never opened in development has been tested
  • Blames the build tool when an unvisited route turns out to be broken
  • Believes any use of a browser global breaks a build, regardless of where it sits
  • Treats a missing configuration value as something only the deployment can hit