119 lines
3.0 KiB
Markdown
119 lines
3.0 KiB
Markdown
---
|
|
title: Use parseAsync for Async Refinements
|
|
impact: CRITICAL
|
|
impactDescription: Using parse() with async refinements throws an error; async validation silently fails or crashes the application
|
|
tags: parse, async, parseAsync, refinement
|
|
---
|
|
|
|
## Use parseAsync for Async Refinements
|
|
|
|
If your schema uses `refine()` or `superRefine()` with async validation (like database lookups), you must use `parseAsync()` or `safeParseAsync()`. Using synchronous `parse()` with async refinements throws an error.
|
|
|
|
**Incorrect (sync parse with async refinement):**
|
|
|
|
```typescript
|
|
import { z } from 'zod'
|
|
|
|
const userSchema = z.object({
|
|
email: z.string().email(),
|
|
username: z.string().min(3),
|
|
}).refine(
|
|
async (data) => {
|
|
// Async database check
|
|
const exists = await db.users.findByEmail(data.email)
|
|
return !exists
|
|
},
|
|
{ message: 'Email already registered' }
|
|
)
|
|
|
|
// This throws an error!
|
|
const user = userSchema.parse(formData)
|
|
// Error: Async refinement encountered during synchronous parse operation.
|
|
// Use .parseAsync instead.
|
|
```
|
|
|
|
**Correct (using parseAsync):**
|
|
|
|
```typescript
|
|
import { z } from 'zod'
|
|
|
|
const userSchema = z.object({
|
|
email: z.string().email(),
|
|
username: z.string().min(3),
|
|
}).refine(
|
|
async (data) => {
|
|
const exists = await db.users.findByEmail(data.email)
|
|
return !exists
|
|
},
|
|
{ message: 'Email already registered' }
|
|
)
|
|
|
|
// Use parseAsync for async refinements
|
|
const user = await userSchema.parseAsync(formData)
|
|
|
|
// Or safeParseAsync for error handling
|
|
const result = await userSchema.safeParseAsync(formData)
|
|
if (!result.success) {
|
|
console.log(result.error.issues)
|
|
}
|
|
```
|
|
|
|
**Async transforms also require parseAsync:**
|
|
|
|
```typescript
|
|
const enrichedUserSchema = z.object({
|
|
userId: z.string().uuid(),
|
|
}).transform(async (data) => {
|
|
// Async data enrichment
|
|
const user = await db.users.findById(data.userId)
|
|
return {
|
|
...data,
|
|
email: user.email,
|
|
name: user.name,
|
|
}
|
|
})
|
|
|
|
// Must use parseAsync
|
|
const enrichedUser = await enrichedUserSchema.parseAsync({ userId: '123' })
|
|
```
|
|
|
|
**Pattern for API routes:**
|
|
|
|
```typescript
|
|
import { z } from 'zod'
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
|
|
const registerSchema = z.object({
|
|
email: z.string().email(),
|
|
password: z.string().min(8),
|
|
}).superRefine(async (data, ctx) => {
|
|
const existingUser = await db.users.findByEmail(data.email)
|
|
if (existingUser) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ['email'],
|
|
message: 'Email already registered',
|
|
})
|
|
}
|
|
})
|
|
|
|
export async function POST(req: NextRequest) {
|
|
const body = await req.json()
|
|
|
|
// Always use safeParseAsync with async schemas
|
|
const result = await registerSchema.safeParseAsync(body)
|
|
|
|
if (!result.success) {
|
|
return NextResponse.json({ errors: result.error.issues }, { status: 400 })
|
|
}
|
|
|
|
// Proceed with registration
|
|
}
|
|
```
|
|
|
|
**When NOT to use this pattern:**
|
|
- Schemas with only synchronous validation (use parse/safeParse)
|
|
- When async validation can be moved outside Zod (validate, then check)
|
|
|
|
Reference: [Zod API - parseAsync](https://zod.dev/api#parseasync)
|