--- title: Use flatten() for Form Error Display impact: HIGH impactDescription: Raw ZodError.issues requires manual path parsing; flatten() provides field-keyed errors ready for form display tags: error, flatten, forms, user-experience --- ## Use flatten() for Form Error Display `ZodError.issues` is an array that requires manual processing to map errors to form fields. `ZodError.flatten()` returns an object with `fieldErrors` keyed by field name, ready for form libraries and UI display. **Incorrect (manual issue processing):** ```typescript import { z } from 'zod' const formSchema = z.object({ email: z.string().email('Invalid email'), password: z.string().min(8, 'Password too short'), profile: z.object({ name: z.string().min(1, 'Name required'), }), }) function getFieldErrors(error: z.ZodError) { const errors: Record = {} for (const issue of error.issues) { // Manual path joining - error prone const field = issue.path.join('.') if (!errors[field]) { errors[field] = issue.message } } return errors } const result = formSchema.safeParse(data) if (!result.success) { const errors = getFieldErrors(result.error) // { email: 'Invalid email', 'profile.name': 'Name required' } } ``` **Correct (using flatten):** ```typescript import { z } from 'zod' const formSchema = z.object({ email: z.string().email('Invalid email'), password: z.string().min(8, 'Password too short'), profile: z.object({ name: z.string().min(1, 'Name required'), }), }) const result = formSchema.safeParse(data) if (!result.success) { const { formErrors, fieldErrors } = result.error.flatten() // formErrors: string[] - top-level errors (from .refine on the object) // fieldErrors: { [key]: string[] } - errors by field // Ready for form display console.log(fieldErrors) // { // email: ['Invalid email'], // password: ['Password too short'], // 'profile.name': ['Name required'] // } } ``` **With React Hook Form:** ```typescript import { zodResolver } from '@hookform/resolvers/zod' import { useForm } from 'react-hook-form' const { register, formState: { errors } } = useForm({ resolver: zodResolver(formSchema), }) // errors are already flattened by the resolver // // {errors.email && {errors.email.message}} ``` **Customizing flatten output:** ```typescript const flattened = result.error.flatten((issue) => ({ message: issue.message, code: issue.code, })) // fieldErrors now contains custom objects // { // email: [{ message: 'Invalid email', code: 'invalid_string' }], // } ``` **For deeply nested objects, use format():** ```typescript const result = formSchema.safeParse(data) if (!result.success) { const formatted = result.error.format() // { // _errors: [], // email: { _errors: ['Invalid email'] }, // profile: { // _errors: [], // name: { _errors: ['Name required'] } // } // } // Access nested errors naturally formatted.profile?.name?._errors // ['Name required'] } ``` **When NOT to use this pattern:** - When you need access to full issue metadata (code, path as array) - When using a form library that expects different error format Reference: [Zod Error Handling](https://zod.dev/error-handling)