119 lines
3.4 KiB
Markdown
119 lines
3.4 KiB
Markdown
---
|
|
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)
|