--- title: Provide Custom Error Messages impact: HIGH impactDescription: Default messages like "Expected string, received number" confuse users; custom messages like "Email is required" are actionable tags: error, messages, user-experience, validation --- ## Provide Custom Error Messages Zod's default error messages are technical and confusing for end users. Provide custom messages that are clear, specific, and actionable. This dramatically improves user experience when validation fails. **Incorrect (default error messages):** ```typescript import { z } from 'zod' const signupSchema = z.object({ email: z.string().email(), password: z.string().min(8), age: z.number().min(18), }) signupSchema.parse({ email: 'bad', password: '123', age: 15 }) // ZodError issues: // - "Invalid email" // - "String must contain at least 8 character(s)" // - "Number must be greater than or equal to 18" // Users see: "String must contain at least 8 character(s)" - what string? ``` **Correct (custom error messages):** ```typescript import { z } from 'zod' const signupSchema = z.object({ email: z.string({ required_error: 'Email is required', invalid_type_error: 'Email must be text', }).email('Please enter a valid email address'), password: z.string({ required_error: 'Password is required', }).min(8, 'Password must be at least 8 characters'), age: z.number({ required_error: 'Age is required', invalid_type_error: 'Age must be a number', }).min(18, 'You must be at least 18 years old'), }) signupSchema.parse({ email: 'bad', password: '123', age: 15 }) // ZodError issues: // - "Please enter a valid email address" // - "Password must be at least 8 characters" // - "You must be at least 18 years old" ``` **Message types and when they trigger:** ```typescript const schema = z.string({ // When field is undefined required_error: 'This field is required', // When field is wrong type (e.g., number instead of string) invalid_type_error: 'This field must be text', // Fallback for any other error message: 'Invalid value', }) .min(1, 'Cannot be empty') // When length < 1 .max(100, 'Too long') // When length > 100 .email('Invalid email format') // When format fails ``` **Using error maps for consistent messaging:** ```typescript const customErrorMap: z.ZodErrorMap = (issue, ctx) => { // Customize messages by error code if (issue.code === z.ZodIssueCode.too_small) { if (issue.type === 'string') { return { message: `Must be at least ${issue.minimum} characters` } } if (issue.type === 'number') { return { message: `Must be at least ${issue.minimum}` } } } if (issue.code === z.ZodIssueCode.invalid_type) { if (issue.expected === 'string') { return { message: 'Must be text' } } } // Default to Zod's message return { message: ctx.defaultError } } // Apply globally z.setErrorMap(customErrorMap) // Or per-schema schema.parse(data, { errorMap: customErrorMap }) ``` **Good error message principles:** - Say what's wrong: "Password too short" not "Invalid password" - Say how to fix it: "at least 8 characters" not just "too short" - Use user's language: "email" not "string field at path .email" - Be specific: "Must be a positive number" not "Invalid" **When NOT to use this pattern:** - Internal development scripts where technical errors are fine - When you'll map errors to user-facing messages in the UI layer Reference: [Zod Error Customization](https://zod.dev/error-customization)