Effective form error handling is a key element of building user-friendly web applications. Well-designed error messages not only inform the user about problems, but also indicate how to resolve them. In this module, we'll discuss best practices for error handling in React and Next.js forms, from designing user-friendly messages to advanced validation techniques.
Before diving into implementation details, it's worth understanding the different types of errors that can occur in forms:
Well-designed error messages should meet the following criteria:
Bad: "Validation error" Good: "Password must contain at least 8 characters, including a digit and an uppercase letter"
Bad: "An error occurred parsing JSON: unexpected token at position 42" Good: "A technical problem occurred. Please try again or contact us."
Bad: "Invalid phone number format" Good: "Enter a phone number in the format 123-456-789"
Error messages should be displayed close to the problematic field, not only at the top of the form.
Error messages should be clearly visible and accessible to all users, including those using screen readers.
Let's move on to practical examples of implementing error handling across different form approaches.
In native React forms, we can handle errors using state:
1import { useState, FormEvent, ChangeEvent } from 'react';
2
3type FormErrors = {
4 name?: string;
5 email?: string;
6 password?: string;
7 generalError?: string;
8};
9
10export default function SignupForm() {
11 const [formData, setFormData] = useState({
12 name: '',
13 email: '',
14 password: ''
15 });
16
17 const [errors, setErrors] = useState<FormErrors>({});
18 const [isSubmitting, setIsSubmitting] = useState(false);
19 const [submitSuccess, setSubmitSuccess] = useState(false);
20
21 const validateForm = (): boolean => {
22 const newErrors: FormErrors = {};
23 let isValid = true;
24
25 // Validation imienia
26 if (!formData.name.trim()) {
27 newErrors.name = 'Name is required';
28 isValid = false;
29 }
30
31 // Validation email
32 if (!formData.email.trim()) {
33 newErrors.email = 'Email is required';
34 isValid = false;
35 } else if (!/^S+@S+.S+$/.test(formData.email)) {
36 newErrors.email = 'Please provide a valid email address';
37 isValid = false;
38 }
39
40 // Password validation
41 if (!formData.password) {
42 newErrors.password = 'Password is required';
43 isValid = false;
44 } else if (formData.password.length < 8) {
45 newErrors.password = 'Password must be at least 8 characters';
46 isValid = false;
47 } else if (!/[A-Z]/.test(formData.password)) {
48 newErrors.password = 'Password must contain at least one uppercase letter';
49 isValid = false;
50 } else if (!/[0-9]/.test(formData.password)) {
51 newErrors.password = 'Password must contain at least one digit';
52 isValid = false;
53 }
54
55 setErrors(newErrors);
56 return isValid;
57 };
58
59 const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
60 const { name, value } = e.target;
61
62 // Update form data
63 setFormData(prev => ({
64 ...prev,
65 [name]: value
66 }));
67
68 // Remove error for edited field
69 if (errors[name as keyof FormErrors]) {
70 setErrors(prev => {
71 const newErrors = { ...prev };
72 delete newErrors[name as keyof FormErrors];
73 return newErrors;
74 });
75 }
76 };
77
78 const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
79 e.preventDefault();
80
81 // Reset state before submission
82 setErrors({});
83 setSubmitSuccess(false);
84
85 // Validate form
86 if (!validateForm()) {
87 return;
88 }
89
90 setIsSubmitting(true);
91
92 try {
93 // Simulate API submission with random error
94 await new Promise(resolve => setTimeout(resolve, 1000));
95
96 // Simulate random server error (25% chance)
97 if (Math.random() < 0.25) {
98 throw new Error('An error occurred while processing the form');
99 }
100
101 // Simulate business error - already existing email (25% chance)
102 if (Math.random() < 0.25) {
103 setErrors({
104 email: 'This email address is already registered'
105 });
106 return;
107 }
108
109 // Success
110 setSubmitSuccess(true);
111 setFormData({ name: '', email: '', password: '' });
112 console.log('Form sent successfully:', formData);
113 } catch (error) {
114 // Handle server error
115 setErrors({
116 generalError: 'A problem occurred while processing the form. Please try again later.'
117 });
118 console.error('Form submission error:', error);
119 } finally {
120 setIsSubmitting(false);
121 }
122 };
123
124 return (
125 <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
126 <h1 className="text-2xl font-bold mb-6">Registration</h1>
127
128 {submitSuccess && (
129 <div className="mb-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded">
130 Registration was successful! Check your email to activate your account.
131 </div>
132 )}
133
134 {errors.generalError && (
135 <div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded" role="alert">
136 <p className="font-bold">An error occurred</p>
137 <p>{errors.generalError}</p>
138 </div>
139 )}
140
141 <form onSubmit={handleSubmit} className="space-y-4" noValidate>
142 <div>
143 <label htmlFor="name" className="block text-sm font-medium text-gray-700">
144 Full name
145 </label>
146 <input
147 type="text"
148 id="name"
149 name="name"
150 value={formData.name}
151 onChange={handleChange}
152 aria-invalid={Boolean(errors.name)}
153 aria-describedby={errors.name ? 'name-error' : undefined}
154 className={`mt-1 block w-full rounded-md shadow-sm ${errors.name ? 'border-red-500' : 'border-gray-300'}`}
155 />
156 {errors.name && (
157 <p className="mt-1 text-sm text-red-600" id="name-error" role="alert">
158 {errors.name}
159 </p>
160 )}
161 </div>
162
163 <div>
164 <label htmlFor="email" className="block text-sm font-medium text-gray-700">
165 Email
166 </label>
167 <input
168 type="email"
169 id="email"
170 name="email"
171 value={formData.email}
172 onChange={handleChange}
173 aria-invalid={Boolean(errors.email)}
174 aria-describedby={errors.email ? 'email-error' : undefined}
175 className={`mt-1 block w-full rounded-md shadow-sm ${errors.email ? 'border-red-500' : 'border-gray-300'}`}
176 />
177 {errors.email && (
178 <p className="mt-1 text-sm text-red-600" id="email-error" role="alert">
179 {errors.email}
180 </p>
181 )}
182 </div>
183
184 <div>
185 <label htmlFor="password" className="block text-sm font-medium text-gray-700">
186 Password
187 </label>
188 <input
189 type="password"
190 id="password"
191 name="password"
192 value={formData.password}
193 onChange={handleChange}
194 aria-invalid={Boolean(errors.password)}
195 aria-describedby={errors.password ? 'password-error' : 'password-rules'}
196 className={`mt-1 block w-full rounded-md shadow-sm ${errors.password ? 'border-red-500' : 'border-gray-300'}`}
197 />
198 {errors.password ? (
199 <p className="mt-1 text-sm text-red-600" id="password-error" role="alert">
200 {errors.password}
201 </p>
202 ) : (
203 <p className="mt-1 text-xs text-gray-500" id="password-rules">
204 Password should be at least 8 characters, contain an uppercase letter and a digit.
205 </p>
206 )}
207 </div>
208
209 <button
210 type="submit"
211 disabled={isSubmitting}
212 className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
213 >
214 {isSubmitting ? 'Processing...' : 'Sign up'}
215 </button>
216 </form>
217 </div>
218 );
219}Key error handling elements in this example:
aria-invalid and aria-describedby for screen readersReact Hook Form offers advanced error handling capabilities:
1import { useForm, SubmitHandler } from 'react-hook-form';
2import { zodResolver } from '@hookform/resolvers/zod';
3import { z } from 'zod';
4import { useState } from 'react';
5
6// Validation schema
7const signupSchema = z.object({
8 name: z.string().min(1, { message: 'Name is required' }),
9 email: z.string().email({ message: 'Please provide a valid email address' }),
10 password: z.string()
11 .min(8, { message: 'Password must be at least 8 characters' })
12 .regex(/[A-Z]/, { message: 'Password must contain at least one uppercase letter' })
13 .regex(/[0-9]/, { message: 'Password must contain at least one digit' })
14});
15
16type SignupFormInputs = z.infer<typeof signupSchema>;
17
18export default function HookFormSignup() {
19 const [serverError, setServerError] = useState<string | null>(null);
20 const [submitSuccess, setSubmitSuccess] = useState(false);
21
22 const {
23 register,
24 handleSubmit,
25 formState: { errors, isSubmitting, touchedFields, isDirty },
26 reset
27 } = useForm<SignupFormInputs>({
28 resolver: zodResolver(signupSchema),
29 mode: 'onTouched' // Validation on blur
30 });
31
32 const onSubmit: SubmitHandler<SignupFormInputs> = async (data) => {
33 // Reset server errors and success before new attempt
34 setServerError(null);
35 setSubmitSuccess(false);
36
37 try {
38 // Simulate API submission
39 await new Promise(resolve => setTimeout(resolve, 1000));
40
41 // Simulate random server error (25% chance)
42 if (Math.random() < 0.25) {
43 throw new Error('An error occurred while processing the form');
44 }
45
46 // Simulate business error - already existing email (25% chance)
47 if (Math.random() < 0.25) {
48 throw new Error('This email address is already registered');
49 }
50
51 // Success
52 console.log('Form sent successfully:', data);
53 setSubmitSuccess(true);
54 reset(); // Clear form
55 } catch (error) {
56 if (error instanceof Error) {
57 setServerError(error.message);
58 } else {
59 setServerError('An unknown error occurred. Please try again later.');
60 }
61 console.error('Form submission error:', error);
62 }
63 };
64
65 return (
66 <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
67 <h1 className="text-2xl font-bold mb-6">Registration (React Hook Form)</h1>
68
69 {submitSuccess && (
70 <div className="mb-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded">
71 Registration was successful! Check your email to activate your account.
72 </div>
73 )}
74
75 {serverError && (
76 <div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded" role="alert">
77 <p className="font-bold">An error occurred</p>
78 <p>{serverError}</p>
79 </div>
80 )}
81
82 <form onSubmit={handleSubmit(onSubmit)} className="space-y-4" noValidate>
83 <div>
84 <label htmlFor="name" className="block text-sm font-medium text-gray-700">
85 Full name
86 </label>
87 <input
88 id="name"
89 type="text"
90 aria-invalid={Boolean(errors.name)}
91 aria-describedby={errors.name ? 'name-error' : undefined}
92 className={`mt-1 block w-full rounded-md shadow-sm ${errors.name ? 'border-red-500' : touchedFields.name ? 'border-green-500' : 'border-gray-300'}`}
93 {...register('name')}
94 />
95 {errors.name && (
96 <p className="mt-1 text-sm text-red-600" id="name-error" role="alert">
97 {errors.name.message}
98 </p>
99 )}
100 </div>
101
102 <div>
103 <label htmlFor="email" className="block text-sm font-medium text-gray-700">
104 Email
105 </label>
106 <input
107 id="email"
108 type="email"
109 aria-invalid={Boolean(errors.email)}
110 aria-describedby={errors.email ? 'email-error' : undefined}
111 className={`mt-1 block w-full rounded-md shadow-sm ${errors.email ? 'border-red-500' : touchedFields.email ? 'border-green-500' : 'border-gray-300'}`}
112 {...register('email')}
113 />
114 {errors.email && (
115 <p className="mt-1 text-sm text-red-600" id="email-error" role="alert">
116 {errors.email.message}
117 </p>
118 )}
119 </div>
120
121 <div>
122 <label htmlFor="password" className="block text-sm font-medium text-gray-700">
123 Password
124 </label>
125 <input
126 id="password"
127 type="password"
128 aria-invalid={Boolean(errors.password)}
129 aria-describedby={errors.password ? 'password-error' : 'password-rules'}
130 className={`mt-1 block w-full rounded-md shadow-sm ${errors.password ? 'border-red-500' : touchedFields.password ? 'border-green-500' : 'border-gray-300'}`}
131 {...register('password')}
132 />
133 {errors.password ? (
134 <p className="mt-1 text-sm text-red-600" id="password-error" role="alert">
135 {errors.password.message}
136 </p>
137 ) : (
138 <p className="mt-1 text-xs text-gray-500" id="password-rules">
139 Password should be at least 8 characters, contain an uppercase letter and a digit.
140 </p>
141 )}
142 </div>
143
144 <button
145 type="submit"
146 disabled={isSubmitting || !isDirty}
147 className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
148 >
149 {isSubmitting ? 'Processing...' : 'Sign up'}
150 </button>
151 </form>
152 </div>
153 );
154}Key error handling elements in React Hook Form:
isSubmitting, touchedFields, isDirtymode: 'onTouched'In forms using Server Actions in Next.js, error handling is especially important because processing happens on the server:
1// app/actions.ts
2'use server';
3
4import { z } from 'zod';
5
6const contactSchema = z.object({
7 name: z.string().min(1, { message: 'Name is required' }),
8 email: z.string().email({ message: 'Please provide a valid email address' }),
9 subject: z.string().min(3, { message: 'Subject is too short' }),
10 message: z.string().min(10, { message: 'Message must be at least 10 characters' })
11});
12
13export type ContactFormState = {
14 errors?: {
15 name?: string[];
16 email?: string[];
17 subject?: string[];
18 message?: string[];
19 _form?: string[];
20 };
21 message?: string;
22 success?: boolean;
23};
24
25export async function submitContactForm(
26 prevState: ContactFormState,
27 formData: FormData
28): Promise<ContactFormState> {
29 // Convert FormData to object
30 const rawFormData = {
31 name: formData.get('name'),
32 email: formData.get('email'),
33 subject: formData.get('subject'),
34 message: formData.get('message')
35 };
36
37 // Validate data
38 const validationResult = contactSchema.safeParse(rawFormData);
39
40 // If validation fails
41 if (!validationResult.success) {
42 // Convert Zod errors to form format
43 return {
44 errors: validationResult.error.flatten().fieldErrors,
45 message: 'Please fix the errors in the form',
46 success: false
47 };
48 }
49
50 try {
51 // Simulate API delay
52 await new Promise(resolve => setTimeout(resolve, 1000));
53
54 // Simulate random server error (15% chance)
55 if (Math.random() < 0.15) {
56 throw new Error('Problem connecting to the server');
57 }
58
59 // Simulate spam error (10% chance)
60 if (Math.random() < 0.1) {
61 return {
62 errors: {
63 _form: ['Possible spam detected. Please try again later.']
64 },
65 success: false
66 };
67 }
68
69 // Simulate form processing
70 console.log('Processing contact form:', validationResult.data);
71
72 return {
73 message: 'Message was sent successfully. We will respond as soon as possible.',
74 success: true
75 };
76 } catch (error) {
77 // Handle server errors
78 return {
79 errors: {
80 _form: [
81 'An error occurred while processing the form. ' +
82 'Please try again later or contact us directly.'
83 ]
84 },
85 success: false
86 };
87 }
88}And the component using this action:
1// app/contact/page.tsx
2'use client';
3
4import { useRef, useEffect } from 'react';
5import { useFormState, useFormStatus } from 'react-dom';
6import { submitContactForm, type ContactFormState } from '../actions';
7
8// Submit button component with loading state handling
9function SubmitButton() {
10 const { pending } = useFormStatus();
11
12 return (
13 <button
14 type="submit"
15 disabled={pending}
16 aria-disabled={pending}
17 className="w-full py-2 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
18 >
19 {pending ? 'Sending...' : 'Send message'}
20 </button>
21 );
22}
23
24export default function ContactForm() {
25 const formRef = useRef<HTMLFormElement>(null);
26
27 // Initialize form state
28 const initialState: ContactFormState = { errors: {} };
29
30 // Connect with Server Action
31 const [state, formAction] = useFormState(submitContactForm, initialState);
32
33 // Reset form after successful submission
34 useEffect(() => {
35 if (state.success && formRef.current) {
36 formRef.current.reset();
37
38 // Focus on success message for screen readers
39 const successMessage = document.getElementById('success-message');
40 if (successMessage) {
41 successMessage.focus();
42 }
43 }
44 }, [state.success]);
45
46 return (
47 <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
48 <h1 className="text-2xl font-bold mb-6">Contact us</h1>
49
50 {state.success && (
51 <div
52 id="success-message"
53 tabIndex={-1}
54 className="mb-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded"
55 role="alert"
56 >
57 {state.message}
58 </div>
59 )}
60
61 {state.errors?._form && (
62 <div className="mb-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded" role="alert">
63 <ul className="list-disc pl-5">
64 {state.errors._form.map((error, index) => (
65 <li key={index}>{error}</li>
66 ))}
67 </ul>
68 </div>
69 )}
70
71 <form ref={formRef} action={formAction} className="space-y-4" noValidate>
72 <div>
73 <label htmlFor="name" className="block text-sm font-medium text-gray-700">
74 Full name
75 </label>
76 <input
77 type="text"
78 id="name"
79 name="name"
80 aria-invalid={Boolean(state.errors?.name)}
81 aria-describedby={state.errors?.name ? 'name-error' : undefined}
82 className={`mt-1 block w-full rounded-md shadow-sm ${state.errors?.name ? 'border-red-500' : 'border-gray-300'}`}
83 />
84 {state.errors?.name && (
85 <p className="mt-1 text-sm text-red-600" id="name-error" role="alert">
86 {state.errors.name[0]}
87 </p>
88 )}
89 </div>
90
91 <div>
92 <label htmlFor="email" className="block text-sm font-medium text-gray-700">
93 Email
94 </label>
95 <input
96 type="email"
97 id="email"
98 name="email"
99 aria-invalid={Boolean(state.errors?.email)}
100 aria-describedby={state.errors?.email ? 'email-error' : undefined}
101 className={`mt-1 block w-full rounded-md shadow-sm ${state.errors?.email ? 'border-red-500' : 'border-gray-300'}`}
102 />
103 {state.errors?.email && (
104 <p className="mt-1 text-sm text-red-600" id="email-error" role="alert">
105 {state.errors.email[0]}
106 </p>
107 )}
108 </div>
109
110 <div>
111 <label htmlFor="subject" className="block text-sm font-medium text-gray-700">
112 Subject
113 </label>
114 <input
115 type="text"
116 id="subject"
117 name="subject"
118 aria-invalid={Boolean(state.errors?.subject)}
119 aria-describedby={state.errors?.subject ? 'subject-error' : undefined}
120 className={`mt-1 block w-full rounded-md shadow-sm ${state.errors?.subject ? 'border-red-500' : 'border-gray-300'}`}
121 />
122 {state.errors?.subject && (
123 <p className="mt-1 text-sm text-red-600" id="subject-error" role="alert">
124 {state.errors.subject[0]}
125 </p>
126 )}
127 </div>
128
129 <div>
130 <label htmlFor="message" className="block text-sm font-medium text-gray-700">
131 Message
132 </label>
133 <textarea
134 id="message"
135 name="message"
136 rows={4}
137 aria-invalid={Boolean(state.errors?.message)}
138 aria-describedby={state.errors?.message ? 'message-error' : undefined}
139 className={`mt-1 block w-full rounded-md shadow-sm ${state.errors?.message ? 'border-red-500' : 'border-gray-300'}`}
140 />
141 {state.errors?.message && (
142 <p className="mt-1 text-sm text-red-600" id="message-error" role="alert">
143 {state.errors.message[0]}
144 </p>
145 )}
146 </div>
147
148 <SubmitButton />
149 </form>
150 </div>
151 );
152}Key error handling elements with Server Actions:
For more complex forms, consider creating an error context:
1// contexts/FormErrorContext.tsx
2import { createContext, useContext, ReactNode, useState } from 'react';
3
4type ErrorsType = Record<string, string[]>;
5
6interface FormErrorContextType {
7 errors: ErrorsType;
8 setErrors: (errors: ErrorsType) => void;
9 clearErrors: () => void;
10 clearFieldError: (fieldName: string) => void;
11 hasErrors: boolean;
12 getFieldErrors: (fieldName: string) => string[] | undefined;
13}
14
15const FormErrorContext = createContext<FormErrorContextType | null>(null);
16
17export function FormErrorProvider({ children }: { children: ReactNode }) {
18 const [errors, setErrors] = useState<ErrorsType>({});
19
20 const clearErrors = () => setErrors({});
21
22 const clearFieldError = (fieldName: string) => {
23 setErrors(prev => {
24 const newErrors = { ...prev };
25 delete newErrors[fieldName];
26 return newErrors;
27 });
28 };
29
30 const getFieldErrors = (fieldName: string) => errors[fieldName];
31
32 const hasErrors = Object.keys(errors).length > 0;
33
34 return (
35 <FormErrorContext.Provider value={{
36 errors,
37 setErrors,
38 clearErrors,
39 clearFieldError,
40 hasErrors,
41 getFieldErrors
42 }}>
43 {children}
44 </FormErrorContext.Provider>
45 );
46}
47
48export function useFormErrors() {
49 const context = useContext(FormErrorContext);
50 if (!context) {
51 throw new Error('useFormErrors must be used within a FormErrorProvider');
52 }
53 return context;
54}1// components/FormError.tsx
2import { useId } from 'react';
3
4interface FormErrorProps {
5 errors?: string[];
6 id?: string;
7}
8
9export function FormError({ errors, id: propId }: FormErrorProps) {
10 const generatedId = useId();
11 const id = propId || generatedId;
12
13 if (!errors || errors.length === 0) {
14 return null;
15 }
16
17 return (
18 <div className="mt-1" id={id} role="alert">
19 {errors.map((error, index) => (
20 <p key={index} className="text-sm text-red-600">
21 {error}
22 </p>
23 ))}
24 </div>
25 );
26}1// components/FormField.tsx
2import { InputHTMLAttributes, ReactNode, useId } from 'react';
3import { FormError } from './FormError';
4import { useFormErrors } from '../contexts/FormErrorContext';
5
6interface FormFieldProps extends InputHTMLAttributes<HTMLInputElement> {
7 label: string;
8 name: string;
9 errors?: string[];
10 children?: ReactNode;
11 helpText?: string;
12}
13
14export function FormField({
15 label,
16 name,
17 errors: propErrors,
18 children,
19 helpText,
20 ...rest
21}: FormFieldProps) {
22 const fieldId = useId();
23 const errorId = `${fieldId}-error`;
24 const helpId = `${fieldId}-help`;
25
26 // We can use the errors context if errors are not passed as a prop
27 const { getFieldErrors } = useFormErrors();
28 const errors = propErrors || getFieldErrors(name);
29
30 const hasError = errors && errors.length > 0;
31
32 // Determine which element describes this field (error or helper text)
33 const ariaDescribedBy = hasError ? errorId : helpText ? helpId : undefined;
34
35 return (
36 <div>
37 <label htmlFor={fieldId} className="block text-sm font-medium text-gray-700">
38 {label}
39 </label>
40
41 <div className="mt-1">
42 {children ? (
43 // If children are passed (e.g., custom fields)
44 children
45 ) : (
46 // Render input by default
47 <input
48 id={fieldId}
49 name={name}
50 aria-invalid={hasError}
51 aria-describedby={ariaDescribedBy}
52 className={`block w-full rounded-md shadow-sm ${hasError ? 'border-red-500' : 'border-gray-300'}`}
53 {...rest}
54 />
55 )}
56 </div>
57
58 {hasError ? (
59 // Show errors
60 <FormError errors={errors} id={errorId} />
61 ) : helpText ? (
62 // Show help text if there are no errors
63 <p className="mt-1 text-xs text-gray-500" id={helpId}>
64 {helpText}
65 </p>
66 ) : null}
67 </div>
68 );
69}1// app/registration/page.tsx
2import { FormErrorProvider } from '../contexts/FormErrorContext';
3import { FormField } from '../components/FormField';
4
5export default function RegistrationPage() {
6 return (
7 <FormErrorProvider>
8 <div className="max-w-md mx-auto p-6 bg-white rounded-lg shadow-md">
9 <h1 className="text-2xl font-bold mb-6">Registration</h1>
10
11 <form className="space-y-4" noValidate>
12 <FormField
13 label="Full name"
14 name="name"
15 type="text"
16 required
17 helpText="Enter your full name"
18 />
19
20 <FormField
21 label="Email"
22 name="email"
23 type="email"
24 required
25 helpText="This address will be used to log into the account"
26 />
27
28 <FormField
29 label="Password"
30 name="password"
31 type="password"
32 required
33 helpText="Password should be at least 8 characters, include an uppercase letter and a digit"
34 />
35
36 {/* Custom field with its own implementation */}
37 <FormField
38 label="Account category"
39 name="accountType"
40 helpText="Choose the account type that fits your needs"
41 >
42 <div className="flex space-x-4">
43 <label className="inline-flex items-center">
44 <input type="radio" name="accountType" value="personal" className="mr-2" />
45 Personal
46 </label>
47 <label className="inline-flex items-center">
48 <input type="radio" name="accountType" value="business" className="mr-2" />
49 Business
50 </label>
51 </div>
52 </FormField>
53
54 <button
55 type="submit"
56 className="w-full py-2 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700"
57 >
58 Sign up
59 </button>
60 </form>
61 </div>
62 </FormErrorProvider>
63 );
64}Different error handling strategies can significantly affect the user experience:
Real-time validation:
On-submit validation:
Best approach: Compromise - validation on blur or after the first submit attempt
Client-side validation:
Server-side validation:
Best approach: Validation at both levels - client for UX, server for security
Individual field validation:
Whole form validation:
Best approach: Field validation during input and full validation on submit
Group related errors - place whole-form errors at the top, specific field errors next to the fields
Maintain visual consistency - use the same colors, icons, and styles for errors throughout the application
Perfect your messages - instead of "Invalid data", write exactly what's wrong and how to fix it
Preserve data on errors - don't reset the entire form after an error occurs
Prioritize errors - show critical errors first, then less important ones
Auto-scroll to errors - especially in long forms, automatically scroll to the first error
Use ARIA - use
aria-invalid, aria-describedby attributes and role="alert" for accessibilityMark required fields - clearly indicate which fields are required, ideally before the user starts filling out the form
Provide examples - for complex formats (e.g., date, phone number) provide examples
Test with users - regularly test forms with real users to identify problems
Effective error handling is especially important for users with disabilities:
Use ARIA attributes
aria-invalid="true" for invalid fieldsaria-describedby linking the field to the error messagerole="alert" for error messages that should be read immediatelyDon't rely on color alone
Use
and <fieldset><legend>
<fieldset><legend>Correct tab order
Error summary
Effective form error handling is much more than just informing the user about problems. It's a comprehensive approach that combines:
Remember that the goal is not only to detect errors, but above all to help the user correct them effectively and successfully complete the form. Well-designed error handling can significantly improve conversion rates and user satisfaction.