Files
2025-11-30 08:25:27 +08:00

4.8 KiB

Error Handling Guide

Complete guide for handling and displaying form errors.


Error Display Patterns

<input {...register('email')} />
{errors.email && (
  <span role="alert" className="text-red-600">
    {errors.email.message}
  </span>
)}

2. Error Summary (Accessibility Best Practice)

{Object.keys(errors).length > 0 && (
  <div role="alert" aria-live="assertive" className="error-summary">
    <h3>Please fix the following errors:</h3>
    <ul>
      {Object.entries(errors).map(([field, error]) => (
        <li key={field}>
          <strong>{field}:</strong> {error.message}
        </li>
      ))}
    </ul>
  </div>
)}

3. Toast Notifications

const onError = (errors) => {
  toast.error(`Please fix ${Object.keys(errors).length} errors`)
}

<form onSubmit={handleSubmit(onSubmit, onError)}>

ARIA Attributes

Required Attributes

<input
  {...register('email')}
  aria-invalid={errors.email ? 'true' : 'false'}
  aria-describedby={errors.email ? 'email-error' : undefined}
  aria-required="true"
/>
{errors.email && (
  <span id="email-error" role="alert">
    {errors.email.message}
  </span>
)}

Custom Error Messages

Method 1: In Zod Schema

const schema = z.object({
  email: z.string()
    .min(1, 'Email is required')
    .email('Please enter a valid email address'),
  password: z.string()
    .min(8, { message: 'Password must be at least 8 characters long' }),
})

Method 2: Custom Error Map

const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
  switch (issue.code) {
    case z.ZodIssueCode.too_small:
      return { message: `Must be at least ${issue.minimum} characters` }
    case z.ZodIssueCode.invalid_string:
      if (issue.validation === 'email') {
        return { message: 'Please enter a valid email address' }
      }
      break
    default:
      return { message: ctx.defaultError }
  }
}

z.setErrorMap(customErrorMap)

Error Formatting

Flatten Errors for Forms

try {
  schema.parse(data)
} catch (error) {
  if (error instanceof z.ZodError) {
    const formattedErrors = error.flatten().fieldErrors
    // Result: { email: ['Invalid email'], password: ['Too short'] }
  }
}

Format Errors for Display

const formatError = (error: FieldError): string => {
  switch (error.type) {
    case 'required':
      return 'This field is required'
    case 'min':
      return `Minimum length is ${error.message}`
    case 'pattern':
      return 'Invalid format'
    default:
      return error.message || 'Invalid value'
  }
}

Server Error Integration

const onSubmit = async (data) => {
  try {
    const response = await fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify(data),
    })

    const result = await response.json()

    if (!result.success && result.errors) {
      // Map server errors to form fields
      Object.entries(result.errors).forEach(([field, message]) => {
        setError(field, {
          type: 'server',
          message: Array.isArray(message) ? message[0] : message,
        })
      })
    }
  } catch (error) {
    // Network error
    setError('root', {
      type: 'server',
      message: 'Unable to connect. Please try again.',
    })
  }
}

Error Persistence

Clear Errors on Input Change

<input
  {...register('email')}
  onChange={(e) => {
    register('email').onChange(e)
    clearErrors('email') // Clear error when user starts typing
  }}
/>

Clear All Errors on Submit Success

const onSubmit = async (data) => {
  const success = await submitData(data)
  if (success) {
    reset() // Clears form and errors
  }
}

Internationalization (i18n)

import { useTranslation } from 'react-i18next'

const { t } = useTranslation()

const schema = z.object({
  email: z.string().email(t('errors.invalidEmail')),
  password: z.string().min(8, t('errors.passwordTooShort')),
})

Error Components

Reusable Error Display

function FormError({ error }: { error?: FieldError }) {
  if (!error) return null

  return (
    <div role="alert" className="error">
      <svg className="icon">...</svg>
      <span>{error.message}</span>
    </div>
  )
}

// Usage
<FormError error={errors.email} />

Field Group with Error

function FieldGroup({ name, label, type = 'text', register, errors }) {
  return (
    <div className="field-group">
      <label htmlFor={name}>{label}</label>
      <input
        id={name}
        type={type}
        {...register(name)}
        aria-invalid={errors[name] ? 'true' : 'false'}
      />
      {errors[name] && <FormError error={errors[name]} />}
    </div>
  )
}

Official Docs: https://react-hook-form.com/