</>CodeWithKarani

Functions Cannot Be Passed to Client Components: the RSC Boundary Explained

Karani GeoffreyKarani Geoffrey7 min read

You come to the Next.js App Router from the Pages Router, or from plain React, and you do the most natural thing in the world: you write a function in a parent component and pass it to a child as a prop. A click handler, a formatter, a callback. In React this has always just worked. In a Server Component it throws:

Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". [function]

The error is accurate but unhelpful in the moment, because it does not name the prop or the line, and sometimes the offending function is not even yours. It can come from inside a third-party component you dropped in and configured with a callback. So you sit there staring at a component that looks identical to a hundred you have written before, wondering what changed.

What changed is that there is now a wire between the server and the browser, and functions cannot travel across it. Once you hold that one idea, the error stops being mysterious and the two real fixes become obvious.

A Server Component renders to a serialized payload that is sent to the browser. Functions cannot be serialized, so passing one as a prop to a Client Component throws. Two real fixes: (1) if the function must run on the server, mark it with "use server" to make it a Server Action, which is safe to pass; or (2) if it is UI logic, define it inside the Client Component instead of passing it down. Do not turn the whole page into a Client Component just to silence the error.

The exact error and where it comes from

The full message is:

Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server". [function]

It fires when a Server Component (the default in the App Router: any component without "use client" at the top of its file) passes a plain function as a prop to a Client Component. A minimal reproduction:

// app/page.tsx  (a Server Component by default)
import Button from "./button";

export default function Page() {
  function handleClick() {          // a plain function on the server
    console.log("clicked");
  }
  return <Button onClick={handleClick} />;   // throws
}
// app/button.tsx
"use client";
export default function Button({ onClick }: { onClick: () => void }) {
  return <button onClick={onClick}>Click</button>;
}

Why this happens: the serialization boundary

In the App Router, Server Components run only on the server. They never ship to the browser. What ships is the result of rendering them: a serialized description of the UI, plus the props that Client Components need in order to hydrate. That serialized payload has to survive being turned into a stream of bytes on the server and reconstructed in the browser.

Serialization is the whole constraint. A value can cross the boundary only if it can be written out and read back. Data can: strings, numbers, booleans, null, plain objects and arrays of those, and a few special-cased types React knows how to encode (including Promises, so you can pass server-fetched data down as it resolves). A function cannot. A function is live code with a closure over server-side scope; there is no meaningful way to serialize "the ability to run this code" and rebuild it in a different runtime that does not have access to that scope. So React refuses, loudly, rather than silently dropping it.

Server (Server Component) "email" (string) { user } (object) handleClick (function) "use server" action wire Browser (Client Component) a Server Action passes as a reference the client can call
Data and Server Actions cross the wire. A plain function has a closure over server scope that cannot be rebuilt in the browser, so it is blocked.

This is the same underlying rule that trips other non-serializable values: class instances, Symbols, and functions all throw at this boundary. The general mental model is worth memorising: a Server Component renders to a serialized payload; a Client Component hydrates from it; anything that cannot survive that round trip cannot be a prop.

The two real fixes

Fix 1: Make it a Server Action with "use server"

If the function is meant to run on the server (submit a form, write to the database, revalidate a cache), mark it as a Server Action. React then passes a serializable reference to the client; when the client calls it, the actual code runs back on the server. Add the directive at the top of the function body, or in a separate file with "use server" at the top:

// app/actions.ts
"use server";
export async function saveEmail(formData: FormData) {
  const email = formData.get("email");
  // ...write to DB on the server
}
// app/page.tsx  (Server Component)
import { saveEmail } from "./actions";

export default function Page() {
  return (
    <form action={saveEmail}>
      <input name="email" />
      <button>Save</button>
    </form>
  );
}

This is the right fix when the work belongs on the server. It is not a way to run arbitrary client-side UI logic; a Server Action is a network call, not a local callback.

Fix 2: Define the function inside the Client Component

Most of the time the function is pure UI: a click handler, a filter, a formatter that only cares about browser state. It has no business being defined on the server. Move it down into the Client Component where it belongs:

// app/button.tsx
"use client";
export default function Button() {
  function handleClick() {     // defined in the browser, never crosses the wire
    console.log("clicked");
  }
  return <button onClick={handleClick}>Click</button>;
}

Now the Server Component just renders <Button /> with no function prop, and there is nothing to serialize. Pass the data the handler needs (a user id, a label) as props, since data serializes fine, and keep the behaviour in the client.

The third-party library case

Sometimes you never wrote the offending function. You render a library component from a Server Component and pass it a config callback, for example an analytics component with a beforeSend hook, and the error points at code you cannot edit. The fix is to wrap the library component in your own thin Client Component:

// app/analytics.tsx
"use client";
import { Analytics } from "some-analytics-lib";

export default function ClientAnalytics() {
  return <Analytics beforeSend={(e) => /* runs in the browser */ e} />;
}

Because the callback is now defined and passed entirely within a Client Component, it never crosses the boundary. Render <ClientAnalytics /> from your server layout. This wrapper pattern is the standard escape hatch for any client-only library used from a server tree.

Verification

Confirm the error is gone and, more importantly, that you did not fix it by dragging the whole tree to the client:

npm run build

A clean build with no serialization error means the boundary is respected. Then check you kept your Server Components as Server Components: search for the client directive and make sure you only added it where a component genuinely needs browser APIs or interactivity.

# every file that opted into the client runtime
grep -rl "use client" app/ | sort

If that list grew by one small leaf component (the button, the analytics wrapper) you fixed it correctly. If it now includes your page or layout, you took the shortcut this article warns against.

What people get wrong

Slapping "use client" on the page. The most common "fix" is to add the client directive to the top of the file that threw. It silences the error because now nothing is a Server Component, so nothing serializes. It also throws away the entire point of the App Router: server rendering, smaller bundles, direct data access. You have downgraded a Server Component to a client one to avoid moving a three-line function. Move the function instead.

Marking UI handlers as "use server". A Server Action is a network round trip to your server. Using one for a onClick that just toggles a menu is absurd: every click becomes a server request. "use server" is for work that must happen on the server, not for silencing the error on client-only logic.

Assuming the error names the prop. It does not, reliably, and the function may live inside a dependency. When you cannot see it, look for the nearest boundary: which Client Component is being rendered from server code with a function-shaped prop, including config callbacks on third-party components.

Confusing this with the TypeScript-level miss. TypeScript will not always catch a non-serializable prop before runtime; the error is a runtime boundary check. If you want it caught earlier, the Next.js TS plugin that misses serializable props on default exports covers why the static check has gaps and how to tighten it.

When it is still broken

  • You moved the function but still see it. Another prop on the same component is also non-serializable: a class instance, a Date passed in a way React does not special-case, a Symbol, or a nested object containing a function. Audit every prop crossing that boundary, not just the obvious callback.
  • It only happens with Turbopack in dev. Some boundary and hydration edge cases have differed between dev bundlers. If a specific dev-only remount or serialization quirk appears, compare against a production build and see the Turbopack dev infinite suspense remount writeup for related behaviour.
  • A Server Action prop still complains. The action must be defined in a module that starts with "use server", or be an inline function whose own body begins with the directive. Exporting a plain async function from a Server Component file is not enough.
  • The library gives you no way in. If a component insists on a function prop and cannot be wrapped on the client, render it only inside a Client Component subtree and pass any server data down as serializable props first.

Frequently asked questions

Why can't I pass a function as a prop to a Client Component in Next.js?
Because a Server Component renders to a serialized payload sent to the browser, and functions cannot be serialized. A function is live code with a closure over server scope that cannot be rebuilt in the browser, so React throws rather than silently dropping it. Data like strings, numbers and plain objects can cross the boundary; functions cannot.
How do I fix 'Functions cannot be passed directly to Client Components'?
Two real fixes. If the function must run on the server, mark it with 'use server' so it becomes a Server Action, which passes as a callable reference. If it is UI logic, define it inside the Client Component instead of passing it down from the server. Do not add 'use client' to the whole page to silence it.
Should I add 'use client' to the page to fix this error?
No. That turns the Server Component into a Client Component so nothing serializes, but it throws away server rendering, smaller bundles and direct data access. Move the small function into the leaf Client Component that needs it instead of downgrading the whole page.
A third-party component triggers this error and I did not write the function. What do I do?
Wrap the library component in your own thin Client Component that has 'use client' at the top, and pass the callback there. Because the callback is now defined and used entirely within a Client Component, it never crosses the server-to-client boundary. Render your wrapper from the server tree.
#Next.js#React#Server Components#Server Actions#App Router
Keep reading

Related articles