EdgeCases Logo
Apr 2026
Next.js
Expert
11 min read

Next.js RSC Serialization Limits

What can (and cannot) cross the server-client boundary in React Server Components

nextjs
react-server-components
rsc
serialization
client-components
server-components
props
error-handling

React Server Components serialize their rendered output and pass it to the browser over the network. This serialization requirement creates strict boundaries on what data you can pass from server to client. The key insight: only JSON-serializable data can cross the RSC boundary. Functions, classes, and complex objects silently fail with cryptic errors.

The Serialization Boundary

When a Server Component renders, Next.js uses React Server Components serialization to convert the component tree into a format that can be transmitted over HTTP and rendered in the browser. This process has strict rules:

// ✅ Serializable - works fine
export default async function ServerComponent() {
  const data = {
    string: "hello",
    number: 42,
    array: [1, 2, 3],
    object: { nested: { value: true } },
    null: null,
    date: new Date().toISOString()  // Stringified date
  };

  return

The serialization happens at the boundary between Server Component and Client Component. If you import and use a Client Component inside a Server Component, any props you pass must be serializable.

What Can Be Serialized

Primitives and Plain Objects

JSON-serializable types work without issues:

  • Strings, numbers, booleans, null, undefined - Primitive types
  • Arrays - Containing serializable values
  • Plain objects - With serializable properties
  • Date strings - date.toISOString() (not Date objects)
// ✅ Correct date handling
export default async function ServerComponent() {
  const post = await db.post.findFirst();
  return

Server Actions and Event Handlers

Starting with Next.js 14, you can pass Server Actions as props to Client Components:

// ✅ Server Actions are serializable
import { createUser } from '@/app/actions';

export default function ServerComponent() {
  return

Server Actions are serialized as special references that Next.js can re-hydrate on the client. Regular functions are not serializable.

What Cannot Be Serialized

Functions (Except Server Actions)

Regular functions cannot be serialized because they cannot be converted to JSON:

// ❌ Error: Functions cannot be serialized
export default function ServerComponent() {
  const handleClick = () => console.log("clicked");

  return

The error message is misleading: "Objects are not valid as a React child". This actually means "this object cannot be serialized".

Class Instances

Class instances contain methods and internal state that cannot be serialized:

// ❌ Error: Class instances cannot be serialized
class MyClass {
  constructor(value) {
    this.value = value;
  }
  method() {
    return this.value * 2;
  }
}

export default function ServerComponent() {
  const instance = new MyClass(42);

  return

Symbols

Symbols are unique references that cannot be transmitted over the network:

// ❌ Error: Symbols cannot be serialized
export default function ServerComponent() {
  const MY_KEY = Symbol("unique");

  return

Date, Map, Set Objects

Built-in objects like Date, Map, and Set cannot be serialized directly:

// ❌ Error: Date objects cannot be serialized
export default function ServerComponent() {
  const now = new Date();

  return

React Elements and Components

You cannot pass JSX elements or component references from Server to Client:

// ❌ Error: React elements cannot be serialized
export default function ServerComponent() {
  const element = ;

  return

Common Error Messages

"Objects are not valid as a React child"

This confusing error actually means "I tried to serialize this object and failed":

// Causes: "Objects are not valid as a React child"
export default function ServerComponent() {
  const data = { onClick: () => {} };  // Function

  return ;
}

"Only plain objects can be passed to Client Components"

This appears when you try to pass class instances or complex objects:

// Causes: "Only plain objects can be passed"
export default function ServerComponent() {
  const data = new FormData();

  return

"Functions cannot be serialized"

Direct error when passing functions as props:

// Causes: "Functions cannot be serialized"
export default function ServerComponent() {
  return

Error Boundaries and Serialization

When a Server Component throws an error during rendering, React serializes the error object into the RSC stream:

// Error gets serialized, not thrown
export default async function ServerComponent() {
  throw new Error("Something went wrong");

  return

The error is serialized as JSON and transmitted in the RSC stream. During SSR (server-side rendering), this error causes the render to fail. During client-side hydration, the error can be caught by an Error Boundary.

However, Error Boundaries do not work in Server Components. You must wrap Client Components to catch errors:

// ❌ Error Boundary in Server Component - doesn't work
export default function ServerComponent() {
  return (
    
  );
}

// ✅ Error Boundary in Client Component - works
export default function ServerComponent() {
  return

Streaming and Partial Serialization

React Server Components support streaming, which means parts of the tree can serialize and render while other parts are still computing:

// Streaming allows partial serialization
export default async function ServerComponent() {
  const fastData = await db.fastQuery();  // 10ms
  const slowData = await db.slowQuery();  // 1000ms

  return (
    
  );
}

However, if slowData contains non-serializable values, the entire stream fails:

// ❌ Entire stream fails if any part can't serialize
export default async function ServerComponent() {
  const fastData = await db.fastQuery();  // Serializable
  const slowData = { onClick: () => {} };  // Not serializable

  return (
    
  );
}

Suspense boundaries help isolate serialization failures:

// ✅ Suspense isolates the error
export default async function ServerComponent() {
  return (
    
  );
}

Server Actions: The Exception

Server Actions are the only function type that can be serialized. They use special serialization that Next.js can reconstruct on the client:

// ✅ Server Actions serialize correctly
"use server"

export async function createUser(formData: FormData) {
  const name = formData.get("name");
  await db.user.create({ name });
  return { success: true };
}

// Server Component can pass this as a prop
export default function ServerComponent() {
  return

Regular async functions (even with "use server" directive) that are not Server Actions cannot be serialized:

// ❌ Not a Server Action - won't serialize
"use server"

export async function helper() {
  return await db.query();
}

export default function ServerComponent() {
  return

Debugging Serialization Issues

Use TypeScript to Catch Issues Early

Define explicit types for props crossing the boundary:

interface ClientProps {
  data: {
    id: string;
    name: string;
    createdAt: string;  // string, not Date
  };
  onSubmit?: ServerAction;  // Only Server Actions allowed
}

"use client"
export default function ClientComponent({ data, onSubmit }: ClientProps) {
  // TypeScript will warn if you try non-serializable types
}

Console.log Before Serialization

Log data before passing to Client Component to verify structure:

export default async function ServerComponent() {
  const data = await fetchData();
  console.log("Data to serialize:", data);  // Inspect before passing

  return

Test with Non-Streaming SSR First

Disable streaming to see serialization errors immediately:

// next.config.js
module.exports = {
  experimental: {
    // Disable streaming for debugging
    serverComponentsExternalPackages: []
  }
}

Performance Implications

Serialization has overhead. Large objects take time to serialize and deserialize:

  • Deeply nested objects - O(n) serialization cost
  • Large arrays - Each element must be serialized
  • Redundant data - Don't pass more than needed
// ❌ Passing too much data
export default async function ServerComponent() {
  const posts = await db.post.findMany({ take: 1000 });

  return

Use pagination or infinite scroll instead of sending large datasets. Serialize on the server, hydrate incrementally on the client.

The Bottom Line

RSC serialization is a strict boundary that forces you to be intentional about what crosses from server to client:

  • Only JSON-serializable data - Primitives, plain objects, arrays
  • Convert dates to strings - Use toISOString() or getTime()
  • Extract data from classes - Don't pass instances
  • Use Server Actions for functions - Regular functions won't serialize
  • Error Boundaries only work client-side - Wrap Client Components, not Server Components

Embrace the constraint. It encourages better architecture by preventing tight coupling between server and client. If you can't serialize something, ask yourself: should this actually live on the client?

Advertisement

Related Insights

Explore related edge cases and patterns

Next.js
Expert
Partial Prerendering: Streaming + Static Shell
8 min
Next.js
Deep
Next.js Server Actions Error Handling Patterns
9 min
Next.js
Deep
Next.js 'use cache': Explicit Caching with Automatic Keys
9 min
Next.js
Deep
Next.js Server Actions Error Handling Patterns
9 min

Advertisement