--- title: Return False Instead of Throwing in Refine impact: HIGH impactDescription: Throwing in refine stops validation early, hiding other errors; returning false allows Zod to collect all issues tags: error, refine, validation, best-practices --- ## Return False Instead of Throwing in Refine When using `.refine()` for custom validation, return `false` for invalid data instead of throwing an error. Throwing stops validation immediately, preventing Zod from collecting other validation errors. This results in poor UX where users fix one error only to discover another. **Incorrect (throwing in refine):** ```typescript import { z } from 'zod' const passwordSchema = z.object({ password: z.string().min(8), confirmPassword: z.string(), }).refine((data) => { if (data.password !== data.confirmPassword) { // Throwing stops all further validation throw new Error('Passwords do not match') } return true }) const formSchema = z.object({ email: z.string().email(), passwords: passwordSchema, terms: z.boolean().refine((v) => v === true, 'Must accept terms'), }) // If passwords don't match, user never learns about other errors formSchema.safeParse({ email: 'bad-email', passwords: { password: '12345678', confirmPassword: 'different' }, terms: false, }) // Only shows: "Passwords do not match" // Hidden: "Invalid email", "Must accept terms" ``` **Correct (returning false in refine):** ```typescript import { z } from 'zod' const passwordSchema = z.object({ password: z.string().min(8), confirmPassword: z.string(), }).refine( (data) => data.password === data.confirmPassword, { message: 'Passwords do not match', path: ['confirmPassword'] } ) const formSchema = z.object({ email: z.string().email(), passwords: passwordSchema, terms: z.boolean().refine((v) => v === true, 'Must accept terms'), }) // All errors are collected formSchema.safeParse({ email: 'bad-email', passwords: { password: '12345678', confirmPassword: 'different' }, terms: false, }) // Shows all errors: // - "Invalid email" // - "Passwords do not match" // - "Must accept terms" ``` **For multiple validation rules, use superRefine:** ```typescript const passwordSchema = z.string().superRefine((password, ctx) => { // Check multiple rules, report all failures if (password.length < 8) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Password must be at least 8 characters', }) } if (!/[A-Z]/.test(password)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Password must contain an uppercase letter', }) } if (!/[0-9]/.test(password)) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Password must contain a number', }) } // Don't return anything - issues are added via ctx }) passwordSchema.safeParse('weak') // All three errors reported at once ``` **Correct pattern for async validation:** ```typescript const schema = z.object({ email: z.string().email(), }).refine( async (data) => { // Return boolean, don't throw const exists = await checkEmailExists(data.email) return !exists }, { message: 'Email already registered', path: ['email'] } ) ``` **When NOT to use this pattern:** - When you need to abort validation entirely (security issues) - When subsequent validations depend on current check passing Reference: [Zod API - Refine](https://zod.dev/api#refine)