You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
inputValidator validation errors lose structure during serialization - issues array becomes JSON string in error.message #6428
Not a big but rather a feedback / issue I'd like to raise
When inputValidator fails (e.g., with Zod), the validation error is serialized in a way that loses all structure. The Zod issues array is JSON-stringified and stuffed into error.message, making it unusable on the client without manual parsing.
Your minimal, reproducible example
// Server functionexportconstmyFn=createServerFn({method: 'POST'}).inputValidator(z.object({name: z.string().min(2,'Name must be at least 2 characters'),age: z.number().min(18,'Must be 18+'),})).handler(async({ data })=>data)// Client - send invalid datamutation.mutate({data: {name: 'A',age: 15}})
Current behavior
Client receives:
{name: "Error",// Lost ZodError typemessage: '[{"origin":"string","code":"too_small","path":["name"],"message":"Name must be at least 2 characters"},...]',// ↑ JSON string, not structured data// No .issues, no .code - just raw JSON crammed into message}
Expected behavior
Client should receive structured error data:
{code: 'VALIDATION_ERROR',message: 'Validation failed',issues: [{path: ['name'],message: 'Name must be at least 2 characters'},{path: ['age'],message: 'Must be 18+'},]}
Or at minimum, preserve the error structure so error.issues is accessible.
Why this matters
Can't distinguish error types - Is this validation error or server crash? Both are just Error
Can't show field-level errors - Must JSON.parse(error.message) to extract issues
Breaks standard patterns - HTTP APIs return 422 with structured validation errors; this pattern makes that impossible
How other frameworks handle this
tRPC: Preserves Zod error structure via superjson
Remix: Actions return structured data, not thrown errors
Next.js Server Actions: Can return error objects with structure
Suggested solutions
Either:
Serialize known error types (ZodError) with their structure preserved
Return validation failures as data (like HTTP 422 response body) instead of thrown errors
Allow custom error serializers via configuration
Additional context
Related discussion: #2935 - covers broader error typing issues from server functions
Additional finding: Function middleware types prevent custom error responses
While investigating workarounds, I discovered a fundamental type constraint in createMiddleware({ type: 'function' }).
The Problem
Function middleware's .server() handler is typed to ONLY return next() result. Looking at createMiddleware.d.ts line 92-100:
exporttypeFunctionServerResultWithContext<...>={'use functions must return the result of next()': true;// <-- Literal marker enforcing next()'~types': {...};context: ...;sendContext: ...;};
This means middleware cannot return custom error responses like:
// This works at RUNTIME but fails TypeScriptexportconsterrorHandler=createMiddleware({type: 'function'}).server(async({ next })=>{try{returnawaitnext()}catch(error){setResponseStatus(422)return{success: false,error: {code: 'VALIDATION_ERROR', issues }}// TS Error!}},)
Contrast with Request Middleware
Request middleware (type: 'request') CAN return Response (line 182):
Using as never type assertion (cleaner than as any):
return{success: false,error: {...}}asnever
Runtime behavior is correct - TanStack serializes the custom response to the client. But the types don't support this valid use case.
Suggestion
Consider allowing function middleware to return custom responses for error handling, similar to how request middleware can return Response. Error handling middleware is a common pattern that the current types don't accommodate.
YESSS! ive just run into this, and its super unfortunate that the inputValidator replaces the ZodError.
if it kept the ZodError class, we could build a custom serialiser that parses the zod issues on the frontend so we can display nice messages, but now that its just an Error class theres no meaningful way of doing this.
The only way now is to wrap every inputValidator scheme with a custom function that rethrows with a custom error class.
which I understand because its within the standard schema branch, and its cool that it serialises the issues, but if we can't tag it as a validation parse error, what would be the most type-safe/reliable way of knowing its a validation issue?
Maybe start could throw a custom "standard schema" class or something, so that we can at least check its instanceof at least?
You could then create a serializationAdapter that checks for this and then use that on the client to get structured validation errors.
// would just need to be something like this? Issue from standard schemaexportclassSomeCustomErrorClass<TextendsReadonlyArray<Issue>[]>extendsError{publicreadonlyissues: Issue[]constructor(issues: T){this.issues=issues// etc etc}}// then a custom serialiserexportconstserializedZodIssuesAdapter=createSerializationAdapter({key: "serializedZodIssues",test: (value: unknown)=>{returnvalueinstanceofSomeCustomErrorClass},toSerializable: (value)=>JSON.stringify(value.issues),fromSerializable: (value)=>{constissues=JSON.parse(value)as$ZodIssue[]returnnewSomeCustomErrorClass(issues)},})// in your ui you could then render the issuesfunctionFormFieldErrors({ error }: {error: Error|null}){if(!error||!(errorinstanceofSomeCustomErrorClass))returnnullif(error.issues.length===0)returnnullreturn(<ulclassName="list-disc pl-4 text-muted-foreground">{error.issues.map((issue)=>(<likey={issue.path.join(".")}>{issue.message}for{issue.path.join(".")}</li>))}</ul>)}
Activity
alexander-zuev commentedon Jan 20, 2026
Additional finding: Function middleware types prevent custom error responses
While investigating workarounds, I discovered a fundamental type constraint in
createMiddleware({ type: 'function' }).The Problem
Function middleware's
.server()handler is typed to ONLY returnnext()result. Looking atcreateMiddleware.d.tsline 92-100:This means middleware cannot return custom error responses like:
Contrast with Request Middleware
Request middleware (
type: 'request') CAN returnResponse(line 182):Current Workaround
Using
as nevertype assertion (cleaner thanas any):Runtime behavior is correct - TanStack serializes the custom response to the client. But the types don't support this valid use case.
Suggestion
Consider allowing function middleware to return custom responses for error handling, similar to how request middleware can return
Response. Error handling middleware is a common pattern that the current types don't accommodate.JClackett commentedon Apr 2, 2026
YESSS! ive just run into this, and its super unfortunate that the inputValidator replaces the ZodError.
if it kept the ZodError class, we could build a custom serialiser that parses the zod issues on the frontend so we can display nice messages, but now that its just an Error class theres no meaningful way of doing this.
The only way now is to wrap every inputValidator scheme with a custom function that rethrows with a custom error class.
It's this line here:
router/packages/start-client-core/src/createServerFn.ts
Line 759 in 5a81726
which I understand because its within the standard schema branch, and its cool that it serialises the issues, but if we can't tag it as a validation parse error, what would be the most type-safe/reliable way of knowing its a validation issue?
Maybe start could throw a custom "standard schema" class or something, so that we can at least check its instanceof at least?
You could then create a serializationAdapter that checks for this and then use that on the client to get structured validation errors.
JClackett commentedon Apr 2, 2026
For now im just wrapping every zod schema with a custom parse zod function:
Works well, but just a bit annoying having to remember to use this everytime