Pixuntra
Blog

The 'use server' rule that crashed our production build

4 min read
Next.jsServer Actions

TypeScript passed. ESLint passed. The dev server was fine. Then the production build failed:

Error occurred prerendering page "/contact"
TypeError: Cannot read properties of undefined (reading 'name')

The setup

A contact form using a Server Action with useActionState. The action needs a shape for its state, and the form needs the initial value of that shape. The obvious thing is to define both next to the action:

// app/contact/actions.ts
"use server";

export type ContactState = {
  status: "idle" | "success" | "error";
  message: string;
  errors: Record<string, string>;
  values: Record<string, string>;
};

export const INITIAL_CONTACT_STATE: ContactState = {
  status: "idle",
  message: "",
  errors: {},
  values: {},
};

export async function submitContact(
  prev: ContactState,
  formData: FormData
): Promise<ContactState> {
  // ...
}

And the form imports both:

"use client";
import { INITIAL_CONTACT_STATE, submitContact } from "./actions";

const [state, formAction, pending] = useActionState(
  submitContact,
  INITIAL_CONTACT_STATE
);

// ...later
defaultValue={state.values.name}

This reads perfectly reasonably. It is wrong.

The rule

A module marked "use server" may only export async functions.

That is not a style guideline. "use server" marks a module boundary that the compiler transforms: every export becomes a callable reference the client can invoke over the network. A plain object cannot be represented that way, so it does not survive the transform.

INITIAL_CONTACT_STATE was stripped. The import resolved to undefined. useActionState was initialised with undefined, and state.values.name threw during prerender.

Why it got that far

This is the part worth dwelling on. Every check we had said the code was fine:

  • TypeScript passed. It type-checks the source, where the export exists. It has no model of a compiler transform removing it later.
  • ESLint passed. No rule covers this.
  • Dev server worked. The error only surfaced during static prerendering of /contact at build time.

The only thing that caught it was npm run build. If our CI had run typecheck and lint but not a full production build, this would have shipped.

The fix

Move anything that is not an async function into an ordinary module:

// app/contact/contact-state.ts   ← no "use server"
export type ContactState = { /* ... */ };

export const INITIAL_CONTACT_STATE: ContactState = {
  status: "idle",
  message: "",
  errors: {},
  values: {},
};

export const SUCCESS_MESSAGE = "Thank you — your enquiry is in.";

Then both the action and the client component import from there. The action file exports exactly one thing: an async function.

Types alone would have been fine, incidentally — they are erased at compile time and never reach the transform. It is runtime values that break.

What we would tell someone facing this

"use server" files export async functions. Nothing else. If you want to colocate a constant, resist it and put it in a neighbouring module.

undefined where you expected an object, in a Server Action context, is almost always this. It is a silent failure — no warning at the import site, no build-time complaint until something dereferences it.

Run the production build in CI. Typecheck and lint are necessary and not sufficient. This class of bug — compiler transforms changing what exists at runtime — is invisible to both, and static prerendering is where it surfaces.

Our CI now runs tsc --noEmit, eslint . and next build on every push. The third one is the only reason this cost an afternoon instead of a production incident.

Want this kind of attention on your project?

We build software for startups and established teams — with the performance, accessibility and security work treated as delivery rather than an upsell.