JavaScript and React course Β· Module 16: Error Handling and Suspense
Error Handling in Forms
In this lesson5
Imagine your space station has a crew registration panel. Each form field is like a sensor on the ship -- if the data is incorrect, the system must immediately notify the operator before the launch sequence is initiated. Error handling in forms is a key UX element of every React application.
Form Validation Patterns
In React, we can distinguish two main approaches to form validation:
1. Client-side Validation
Validation performed in the browser before data is sent to the server. It's fast and provides immediate feedback.
1function RegistrationForm() {
2 const [email, setEmail] = useState('');
3 const [errors, setErrors] = useState({});
4
5 const validateEmail = (value) => {
6 if (!value) return 'Email is required';
7 if (!value.includes('@')) return 'Invalid email format';
8 return null;
9 };
10
11 const handleChange = (e) => {
12 const value = e.target.value;
13 setEmail(value);
14 const error = validateEmail(value);
15 setErrors(prev => ({ ...prev, email: error }));
16 };
17
18 return (
19 <div>
20 <input value={email} onChange={handleChange} />
21 {errors.email && <p className="error">{errors.email}</p>}
22 </div>
23 );
24}2. Server-side Validation
The server validates the data after the form is submitted and returns errors. It's essential for security -- client-side validation can be bypassed.
1const handleSubmit = async (formData) => {
2 try {
3 const response = await fetch('/api/register', {
4 method: 'POST',
5 body: JSON.stringify(formData),
6 });
7 const result = await response.json();
8
9 if (!response.ok) {
10 // Server returned validation errors
11 setErrors(result.errors);
12 }
13 } catch (error) {
14 setErrors({ general: 'Connection error with server' });
15 }
16};Displaying Field-Level Errors
Each form field should have its own error message, displayed directly below the field. It's like individual alarms for each system on a spacecraft.
1function CrewRegistration() {
2 const [formData, setFormData] = useState({
3 name: '', callsign: '', rank: ''
4 });
5 const [errors, setErrors] = useState({});
6 const [touched, setTouched] = useState({});
7
8 const validate = (field, value) => {
9 switch (field) {
10 case 'name':
11 if (!value.trim()) return 'Name is required';
12 if (value.length < 2) return 'Name must be at least 2 characters';
13 return null;
14 case 'callsign':
15 if (!value.trim()) return 'Callsign is required';
16 if (!/^[A-Z0-9-]+$/.test(value)) return 'Only uppercase letters, digits, and hyphens';
17 return null;
18 case 'rank':
19 if (!value) return 'Select a rank';
20 return null;
21 default:
22 return null;
23 }
24 };
25
26 const handleBlur = (field) => {
27 setTouched(prev => ({ ...prev, [field]: true }));
28 const error = validate(field, formData[field]);
29 setErrors(prev => ({ ...prev, [field]: error }));
30 };
31
32 const handleChange = (field, value) => {
33 setFormData(prev => ({ ...prev, [field]: value }));
34 // Validate only if the field has been touched
35 if (touched[field]) {
36 const error = validate(field, value);
37 setErrors(prev => ({ ...prev, [field]: error }));
38 }
39 };
40
41 return (
42 <form>
43 <div>
44 <label>Name</label>
45 <input
46 value={formData.name}
47 onChange={(e) => handleChange('name', e.target.value)}
48 onBlur={() => handleBlur('name')}
49 className={errors.name ? 'input-error' : ''}
50 />
51 {touched.name && errors.name && (
52 <p className="field-error">{errors.name}</p>
53 )}
54 </div>
55 {/* Same pattern for remaining fields */}
56 </form>
57 );
58}Notice the touched pattern -- we only show errors after the user has left the field (onBlur). We don't want to scare the pilot with alarms before they've even finished entering data!
Handling Server Errors in Forms
The server can return errors that cannot be predicted on the client side -- e.g., "this email is already taken" or "registration limit exceeded".
1function SubmitWithServerErrors() {
2 const [errors, setErrors] = useState({});
3 const [serverError, setServerError] = useState(null);
4
5 const handleSubmit = async (e) => {
6 e.preventDefault();
7 setServerError(null);
8
9 try {
10 const response = await fetch('/api/crew/register', {
11 method: 'POST',
12 headers: { 'Content-Type': 'application/json' },
13 body: JSON.stringify(formData),
14 });
15
16 const data = await response.json();
17
18 if (!response.ok) {
19 if (data.fieldErrors) {
20 // Field-specific errors (e.g., "email already exists")
21 setErrors(data.fieldErrors);
22 }
23 if (data.message) {
24 // General server error
25 setServerError(data.message);
26 }
27 return;
28 }
29
30 // Success - redirect or show message
31 } catch (error) {
32 setServerError('Failed to connect to the base station');
33 }
34 };
35
36 return (
37 <form onSubmit={handleSubmit}>
38 {serverError && (
39 <div className="server-error-banner">{serverError}</div>
40 )}
41 {/* Form fields with field-level errors */}
42 </form>
43 );
44}try-catch in handleSubmit
Every form submission handler should be wrapped in try-catch. Without it, a network error or unexpected server error will cause an unhandled exception.
1const handleSubmit = async (e) => {
2 e.preventDefault();
3 setIsSubmitting(true);
4 setErrors({});
5
6 try {
7 // 1. Local validation
8 const validationErrors = validateAllFields(formData);
9 if (Object.keys(validationErrors).length > 0) {
10 setErrors(validationErrors);
11 return;
12 }
13
14 // 2. Send to server
15 const response = await submitToServer(formData);
16
17 // 3. Handle server response
18 if (!response.ok) {
19 const errorData = await response.json();
20 throw new Error(errorData.message || 'Server error');
21 }
22
23 // 4. Success
24 setSuccess(true);
25 } catch (error) {
26 // 5. Error handling
27 setErrors({ submit: error.message });
28 } finally {
29 // 6. Always executed - disable loader
30 setIsSubmitting(false);
31 }
32};UX Best Practices for Error Messages
Good error messages in forms should be:
- Specific -- "Password must be at least 8 characters" instead of "Invalid password"
- Helpful -- tell the user HOW to fix the error
- Visible -- display the error near the field it relates to
- Gentle -- don't show all errors at once before the first submit
- Accessible -- use
aria-invalidandaria-describedbyattributes
1<div className="form-field">
2 <label htmlFor="password">Password</label>
3 <input
4 id="password"
5 type="password"
6 aria-invalid={!!errors.password}
7 aria-describedby={errors.password ? 'password-error' : undefined}
8 />
9 {errors.password && (
10 <p id="password-error" className="field-error" role="alert">
11 {errors.password}
12 </p>
13 )}
14</div>Remember -- a form is like a spacecraft's control panel. Every error must be communicated clearly and precisely so that the pilot (user) knows exactly what to fix. Never show mysterious error codes -- always translate them into human-readable language.
Code for this lesson: App.jsx
1import React, { useState } from 'react';
2import './styles.css';
3
4// Crew member registration form with validation
5function CrewRegistrationForm() {
6 const [formData, setFormData] = useState({
7 name: '', callsign: '', email: '', rank: ''
8 });
9 const [errors, setErrors] = useState({});
10 const [touched, setTouched] = useState({});
11 const [serverError, setServerError] = useState(null);
12 const [isSubmitting, setIsSubmitting] = useState(false);
13 const [success, setSuccess] = useState(false);
14
15 const validate = (field, value) => {
16 switch (field) {
17 case 'name':
18 if (!value.trim()) return 'Name is required';
19 if (value.length < 2) return 'Name must have at least 2 characters';
20 return null;
21 case 'callsign':
22 if (!value.trim()) return 'Callsign is required';
23 if (!/^[A-Za-z0-9-]+$/.test(value))
24 return 'Only letters, digits, and hyphens';
25 return null;
26 case 'email':
27 if (!value.trim()) return 'Email is required';
28 if (!value.includes('@')) return 'Invalid email format';
29 return null;
30 case 'rank':
31 if (!value) return 'Choose a rank';
32 return null;
33 default:
34 return null;
35 }
36 };
37
38 const handleChange = (field, value) => {
39 setFormData(prev => ({ ...prev, [field]: value }));
40 if (touched[field]) {
41 const error = validate(field, value);
42 setErrors(prev => ({ ...prev, [field]: error }));
43 }
44 };
45
46 const handleBlur = (field) => {
47 setTouched(prev => ({ ...prev, [field]: true }));
48 const error = validate(field, formData[field]);
49 setErrors(prev => ({ ...prev, [field]: error }));
50 };
51
52 const handleSubmit = async (e) => {
53 e.preventDefault();
54 setServerError(null);
55 setIsSubmitting(true);
56
57 // Validate all fields
58 const allErrors = {};
59 const allTouched = {};
60 for (const field of Object.keys(formData)) {
61 allTouched[field] = true;
62 const error = validate(field, formData[field]);
63 if (error) allErrors[field] = error;
64 }
65 setTouched(allTouched);
66 setErrors(allErrors);
67
68 if (Object.keys(allErrors).length > 0) {
69 setIsSubmitting(false);
70 return;
71 }
72
73 try {
74 // Simulating sending to the server
75 await new Promise(resolve => setTimeout(resolve, 1000));
76
77 // Simulating a server error (50% chance)
78 if (Math.random() > 0.5) {
79 throw new Error('Server rejected registration: callsign already exists');
80 }
81
82 setSuccess(true);
83 } catch (error) {
84 setServerError(error.message);
85 } finally {
86 setIsSubmitting(false);
87 }
88 };
89
90 if (success) {
91 return (
92 <div className="success-panel">
93 <h2>Registration successful!</h2>
94 <p>Welcome aboard, {formData.callsign}!</p>
95 <button onClick={() => { setSuccess(false); setFormData({ name: '', callsign: '', email: '', rank: '' }); setTouched({}); }}>
96 Register another
97 </button>
98 </div>
99 );
100 }
101
102 return (
103 <form onSubmit={handleSubmit} className="form">
104 {serverError && (
105 <div className="server-error">{serverError}</div>
106 )}
107
108 <div className="field">
109 <label>Name</label>
110 <input
111 value={formData.name}
112 onChange={(e) => handleChange('name', e.target.value)}
113 onBlur={() => handleBlur('name')}
114 className={touched.name && errors.name ? 'input-error' : ''}
115 placeholder="e.g. John Smith"
116 />
117 {touched.name && errors.name && (
118 <p className="field-error">{errors.name}</p>
119 )}
120 </div>
121
122 <div className="field">
123 <label>Callsign</label>
124 <input
125 value={formData.callsign}
126 onChange={(e) => handleChange('callsign', e.target.value)}
127 onBlur={() => handleBlur('callsign')}
128 className={touched.callsign && errors.callsign ? 'input-error' : ''}
129 placeholder="Np. NOVA-7"
130 />
131 {touched.callsign && errors.callsign && (
132 <p className="field-error">{errors.callsign}</p>
133 )}
134 </div>
135
136 <div className="field">
137 <label>Email</label>
138 <input
139 value={formData.email}
140 onChange={(e) => handleChange('email', e.target.value)}
141 onBlur={() => handleBlur('email')}
142 className={touched.email && errors.email ? 'input-error' : ''}
143 placeholder="e.g. john@station.space"
144 />
145 {touched.email && errors.email && (
146 <p className="field-error">{errors.email}</p>
147 )}
148 </div>
149
150 <div className="field">
151 <label>Rank</label>
152 <select
153 value={formData.rank}
154 onChange={(e) => handleChange('rank', e.target.value)}
155 onBlur={() => handleBlur('rank')}
156 className={touched.rank && errors.rank ? 'input-error' : ''}
157 >
158 <option value="">-- Choose a rank --</option>
159 <option value="cadet">Cadet</option>
160 <option value="officer">Officer</option>
161 <option value="commander">Commander</option>
162 </select>
163 {touched.rank && errors.rank && (
164 <p className="field-error">{errors.rank}</p>
165 )}
166 </div>
167
168 <button type="submit" className="submit-btn" disabled={isSubmitting}>
169 {isSubmitting ? 'Sending...' : 'Register crew member'}
170 </button>
171 </form>
172 );
173}
174
175export default function App() {
176 return (
177 <div className="app">
178 <h1>Crew Registration</h1>
179 <p className="subtitle">Form validation with error handling</p>
180 <CrewRegistrationForm />
181 </div>
182 );
183}Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is the main advantage of client-side form validation?
2. Why do we use the touched pattern (onBlur) when displaying validation errors?
Hands-on tasks in the game
- Click in order
Click elements in order for the handleSubmit form steps:
- Code editor
Implement a form with validation and error handling
- Horizontal ordering
Arrange the correct syntax for conditionally displaying a field error:
- Vertical ordering
Order the types of form errors from earliest handled to latest: