Two related issues when returning structured error objects from server functions.
1. Type: ValidateSerializableMapped rejects classes with toJSON()
createServerFn().handler(...) rejects return types that contain class instances, even when those classes implement toJSON() and are runtime-serializable.
The static serializability check (ValidateSerializableMapped) sees class methods and rejects the return type, even though the actual value would serialize to plain JSON through toJSON().
import { createServerFn } from '@tanstack/react-start'
class AppError {
readonly _tag = 'AppError'
constructor(readonly message: string) {}
toJSON() { return { _tag: this._tag, message: this.message } }
}
type RpcResult<T, E> = { status: 'ok'; value: T } | { status: 'error'; error: E }
// TS error: "Function may not be serializable"
export const fn = createServerFn().handler(
async (): Promise<RpcResult<string, AppError>> => {
return { status: 'error', error: new AppError('fail') }
}
)
Is there an intended way to tell TanStack Start that a class with toJSON() is serializable? If not, would you consider supporting one of:
toJSON()-aware serializability typing
- custom serializer registration
- an escape hatch for "already serialized" return values
2. Runtime: ShallowErrorPlugin destroys custom properties on Error subclasses
When a server function returns an object containing an Error subclass (even nested inside a plain wrapper), seroval's ShallowErrorPlugin intercepts it and reconstructs as new Error(message) — all custom properties are lost.
class NotFoundError extends Error {
readonly code = 'NOT_FOUND'
readonly entity: string
readonly retryable = false
constructor(entity: string, id: string) {
super(`${entity} "${id}" not found`)
this.entity = entity
}
}
export const fn = createServerFn().handler(async () => {
// Return a plain wrapper with a structured error inside
return {
status: 'error',
error: new NotFoundError('User', '123'),
}
})
Server sends: { status: "error", error: { code: "NOT_FOUND", entity: "User", retryable: false, message: "User \"123\" not found" } }
Client receives: { status: "error", error: Error("User \"123\" not found") } — code, entity, retryable all gone.
The plugin walks the full object tree and catches anything where instanceof Error === true:
// @tanstack/router-core ShallowErrorPlugin
test(value) { return value instanceof Error },
deserialize(node, ctx) { return new Error(ctx.deserialize(node.message)) }
This makes it impossible to pass structured Error subclasses across the server fn boundary without manually breaking the prototype chain first.
Current workaround
For types: mapped type that strips methods and maps unknown to JsonValue.
For runtime: spread error into a plain object to break instanceof Error.
function toServerFnRpc<T, E>(result: Result<T, E>): RpcResult<T, E> {
const serialized = Result.serialize(result)
if (serialized.status === 'error' && typeof serialized.error === 'object' && serialized.error !== null) {
const { stack: _, ...plain } = serialized.error as Record<string, unknown>
return { status: 'error', error: plain } as RpcResult<T, E>
}
return serialized as RpcResult<T, E>
}
Two related issues when returning structured error objects from server functions.
1. Type:
ValidateSerializableMappedrejects classes withtoJSON()createServerFn().handler(...)rejects return types that contain class instances, even when those classes implementtoJSON()and are runtime-serializable.The static serializability check (
ValidateSerializableMapped) sees class methods and rejects the return type, even though the actual value would serialize to plain JSON throughtoJSON().Is there an intended way to tell TanStack Start that a class with
toJSON()is serializable? If not, would you consider supporting one of:toJSON()-aware serializability typing2. Runtime:
ShallowErrorPlugindestroys custom properties on Error subclassesWhen a server function returns an object containing an Error subclass (even nested inside a plain wrapper), seroval's
ShallowErrorPluginintercepts it and reconstructs asnew Error(message)— all custom properties are lost.Server sends:
{ status: "error", error: { code: "NOT_FOUND", entity: "User", retryable: false, message: "User \"123\" not found" } }Client receives:
{ status: "error", error: Error("User \"123\" not found") }—code,entity,retryableall gone.The plugin walks the full object tree and catches anything where
instanceof Error === true:This makes it impossible to pass structured Error subclasses across the server fn boundary without manually breaking the prototype chain first.
Current workaround
For types: mapped type that strips methods and maps
unknowntoJsonValue.For runtime: spread error into a plain object to break
instanceof Error.