How to Fix: Server action causes TypeError when returning a value
A Server Action that works until it returns a value is a strong signal that the response cannot be safely serialized across the React Server Components boundary. In this issue, the action executes, but returning the wrong shape triggers a TypeError because Next.js must serialize the result before sending it back to the client.
Table of Contents
Understanding the Root Cause
The bug happens because Server Actions do not return arbitrary JavaScript values the same way a normal in-process function does. When a client component calls a server action, Next.js has to move that returned value from the server to the browser. That means the result must be compatible with the frameworkâs serialization rules.
If the action returns a value that includes unsupported data, such as a class instance, function, complex prototype, circular object, or another structure React cannot serialize for transport, Next.js fails while preparing the response. The visible symptom is often a TypeError rather than a more descriptive validation error.
In practical terms, the issue is usually caused by returning something other than a plain, transport-safe payload. For example:
- A database object with methods or custom prototypes
- A native Error instance
- A Response object
- A value containing Date, Map, Set, BigInt, or nested unsupported types
- A non-serializable object produced by a library
The safest mental model is this: a server action should return only plain JSON-like data, or avoid returning data entirely and instead trigger cache updates, redirects, or revalidation.
Step-by-Step Solution
The fix is to return a plain object made of strings, numbers, booleans, null, arrays, and other plain objects. If you need richer values, convert them before returning.
1. Reproduce the problematic pattern
'use server'
export async function myAction() {
const result = await someLibraryCall()
return result
}
If result is not serializable, the client call fails when Next.js tries to send it back.
2. Return a plain serializable payload
'use server'
export async function myAction() {
const result = await someLibraryCall()
return {
success: true,
id: String(result.id),
message: result.message ?? null
}
}
This works because the returned structure is a plain object containing transport-safe primitives.
3. Convert unsupported values explicitly
If your action depends on values like Date or custom objects, normalize them first.
'use server'
export async function myAction() {
const user = await getUser()
return {
id: user.id,
name: user.name,
createdAt: user.createdAt.toISOString()
}
}
4. Do not return native Error objects
Returning an actual Error instance is a common source of breakage. Return a plain error state instead.
'use server'
export async function myAction() {
try {
await doWork()
return {
success: true
}
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error'
}
}
}
5. If no return value is needed, return nothing
Many server actions are used for mutation only. In that case, do not send extra data back.
'use server'
import { revalidatePath } from 'next/cache'
export async function updatePost(formData) {
await savePost(formData)
revalidatePath('/posts')
}
This avoids unnecessary serialization entirely.
6. Update the client caller to expect a plain result
'use client'
import { myAction } from './actions'
export default function Example() {
async function handleClick() {
const result = await myAction()
if (!result.success) {
console.error(result.message)
return
}
console.log(result.id)
}
return <button onClick={handleClick}>Run action</button>
}
7. Use a predictable action result shape
A robust pattern is to standardize action responses across your app.
'use server'
type ActionResult<T> = {
success: boolean
data?: T
error?: string
}
export async function createItem(): Promise<ActionResult<{ id: string }>> {
try {
const item = await createRecord()
return {
success: true,
data: {
id: String(item.id)
}
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to create item'
}
}
}
This keeps the action boundary stable and prevents accidental leakage of non-serializable objects.
Common Edge Cases
- Returning database models directly: ORM entities sometimes include symbols, methods, lazy fields, or prototypes. Always map them to plain objects first.
- Date values: Convert them to ISO strings before returning to the client.
- BigInt values: These often break serialization. Convert them to strings.
- Nested unsupported values: Even if the top-level object is plain, one nested field can still trigger the same TypeError.
- Throwing versus returning: If you throw inside a server action, handle it intentionally in the caller or convert it to a plain result object.
- Response or Request objects: These are not valid return values for server actions. Extract the data you need and return only that data.
- Library-specific objects: Validation libraries, database clients, and SDKs may return rich objects that look plain but are not transport-safe.
If you are unsure whether a payload is safe, inspect it and map it manually instead of returning it directly.
FAQ
Can a server action return any JavaScript object?
No. A server action can only return values that Next.js and React can serialize across the server-client boundary. Stick to plain JSON-like structures.
Why does the action work until I add a return statement?
Because the mutation itself runs on the server successfully, but the failure happens later when Next.js tries to serialize the return value and send it back to the client.
Should I throw errors or return error objects from server actions?
For predictable UI flows, returning a plain object such as { success: false, message: '...' } is usually easier to handle. Throwing is still valid, but it should be reserved for cases where you want the error boundary or framework-level handling to take over.
The reliable fix for this issue is simple: treat the Server Action boundary like a network boundary. Return only plain, serializable data, normalize complex values before returning them, and avoid passing library objects directly. If you want to inspect the original reproduction, use the reproduction repository linked from the issue.