Cours JavaScript et React · Module 16: Gestion des erreurs et Suspense

La gestion des erreurs dans les formulaires

5 min de lecture
Dans cette leçon5

Imagine que ta station spatiale dispose d'un panneau d'enregistrement de l'équipage. Chaque champ de formulaire est comme un capteur du vaisseau : si les données sont incorrectes, le système doit immédiatement avertir l'opérateur avant le lancement de la séquence de décollage. La gestion des erreurs dans les formulaires est un élément UX clé de toute application React.

Les modèles de validation des formulaires

Dans React, on peut distinguer deux grandes approches de validation des formulaires :

1. Validation côté client

Validation effectuée dans le navigateur avant l'envoi des données au serveur. Elle est rapide et offre un retour immédiat.

1function RegistrationForm() {
2  const [email, setEmail] = useState('');
3  const [errors, setErrors] = useState({});
4
5  const validateEmail = (value) => {
6    if (!value) return 'L\'email est requis';
7    if (!value.includes('@')) return 'Format d\'email invalide';
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. Validation côté serveur

Le serveur valide les données après l'envoi du formulaire et retourne les erreurs. Elle est essentielle pour la sécurité : la validation côté client peut être contournée.

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      // Le serveur a retourné des erreurs de validation
11      setErrors(result.errors);
12    }
13  } catch (error) {
14    setErrors({ general: 'Erreur de connexion au serveur' });
15  }
16};

Afficher les erreurs au niveau du champ

Chaque champ de formulaire doit avoir son propre message d'erreur, affiché juste sous le champ. C'est comme des alarmes individuelles pour chaque système d'un vaisseau spatial.

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 'Le nom est requis';
12        if (value.length < 2) return 'Le nom doit faire au moins 2 caractères';
13        return null;
14      case 'callsign':
15        if (!value.trim()) return 'L\'indicatif est requis';
16        if (!/^[A-Z0-9-]+$/.test(value)) return 'Uniquement majuscules, chiffres et tirets';
17        return null;
18      case 'rank':
19        if (!value) return 'Choisis un grade';
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    // Valide uniquement si le champ a déjà été touché
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>Nom</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      {/* Même modèle pour les champs restants */}
56    </form>
57  );
58}

Remarque le modèle touched : nous n'affichons les erreurs qu'après que l'utilisateur a quitté le champ (onBlur). On ne veut pas effrayer le pilote avec des alarmes avant même qu'il ait fini de saisir ses données !

Gérer les erreurs serveur dans les formulaires

Le serveur peut retourner des erreurs impossibles à prévoir côté client, par ex. « cet email est déjà pris » ou « limite d'inscriptions atteinte ».

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          // Erreurs spécifiques à un champ (par ex. « email déjà existant »)
21          setErrors(data.fieldErrors);
22        }
23        if (data.message) {
24          // Erreur serveur générale
25          setServerError(data.message);
26        }
27        return;
28      }
29
30      // Succès - redirige ou affiche un message
31    } catch (error) {
32      setServerError('Impossible de se connecter à la station de base');
33    }
34  };
35
36  return (
37    <form onSubmit={handleSubmit}>
38      {serverError && (
39        <div className="server-error-banner">{serverError}</div>
40      )}
41      {/* Champs du formulaire avec erreurs au niveau du champ */}
42    </form>
43  );
44}

try-catch dans handleSubmit

Chaque gestionnaire de soumission de formulaire doit être enveloppé dans un try-catch. Sans cela, une erreur réseau ou une erreur serveur inattendue provoquera une exception non gérée.

1const handleSubmit = async (e) => {
2  e.preventDefault();
3  setIsSubmitting(true);
4  setErrors({});
5
6  try {
7    // 1. Validation locale
8    const validationErrors = validateAllFields(formData);
9    if (Object.keys(validationErrors).length > 0) {
10      setErrors(validationErrors);
11      return;
12    }
13
14    // 2. Envoi au serveur
15    const response = await submitToServer(formData);
16
17    // 3. Traitement de la réponse du serveur
18    if (!response.ok) {
19      const errorData = await response.json();
20      throw new Error(errorData.message || 'Server error');
21    }
22
23    // 4. Succès
24    setSuccess(true);
25  } catch (error) {
26    // 5. Gestion de l'erreur
27    setErrors({ submit: error.message });
28  } finally {
29    // 6. Toujours exécuté - désactive le loader
30    setIsSubmitting(false);
31  }
32};

Bonnes pratiques UX pour les messages d'erreur

De bons messages d'erreur dans les formulaires doivent être :

  1. Précis - « Le mot de passe doit faire au moins 8 caractères » plutôt que « Mot de passe invalide »
  2. Utiles - indiquer à l'utilisateur COMMENT corriger l'erreur
  3. Visibles - afficher l'erreur près du champ concerné
  4. Doux - ne pas montrer toutes les erreurs d'un coup avant la première soumission
  5. Accessibles - utiliser les attributs aria-invalid et aria-describedby
1<div className="form-field">
2  <label htmlFor="password">Mot de passe</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>

Souviens-toi : un formulaire, c'est comme le panneau de contrôle d'un vaisseau spatial. Chaque erreur doit être communiquée de façon claire et précise pour que le pilote (l'utilisateur) sache exactement quoi corriger. Ne montre jamais de codes d'erreur mystérieux : traduis-les toujours en langage compréhensible par un humain.

Code de cette leçon : App.jsx
1import React, { useState } from 'react';
2import './styles.css';
3
4// Formulaire d'enregistrement d'un membre d'équipage avec 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 'Le nom est requis';
19        if (value.length < 2) return 'Le nom doit faire au moins 2 caractères';
20        return null;
21      case 'callsign':
22        if (!value.trim()) return 'L\'indicatif est requis';
23        if (!/^[A-Za-z0-9-]+$/.test(value))
24          return 'Uniquement lettres, chiffres et tirets';
25        return null;
26      case 'email':
27        if (!value.trim()) return 'L\'email est requis';
28        if (!value.includes('@')) return 'Format d\'email invalide';
29        return null;
30      case 'rank':
31        if (!value) return 'Choisis un grade';
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    // Validation de tous les champs
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      // Simulation d'envoi au serveur
75      await new Promise(resolve => setTimeout(resolve, 1000));
76
77      // Simulation d'une erreur serveur (50% de chances)
78      if (Math.random() > 0.5) {
79        throw new Error('Le serveur a rejeté l\'inscription : l\'indicatif existe déjà');
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>Inscription réussie !</h2>
94        <p>Bienvenue à bord, {formData.callsign} !</p>
95        <button onClick={() => { setSuccess(false); setFormData({ name: '', callsign: '', email: '', rank: '' }); setTouched({}); }}>
96          Enregistrer le suivant
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>Nom</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="Ex. Jean Dupont"
116        />
117        {touched.name && errors.name && (
118          <p className="field-error">{errors.name}</p>
119        )}
120      </div>
121
122      <div className="field">
123        <label>Indicatif</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="Ex. 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="Ex. jean@station.cosmos"
144        />
145        {touched.email && errors.email && (
146          <p className="field-error">{errors.email}</p>
147        )}
148      </div>
149
150      <div className="field">
151        <label>Grade</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="">-- Choisis un grade --</option>
159          <option value="cadet">Cadet</option>
160          <option value="officer">Officier</option>
161          <option value="commander">Commandant</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 ? 'Envoi...' : 'Enregistrer le membre d\'équipage'}
170      </button>
171    </form>
172  );
173}
174
175export default function App() {
176  return (
177    <div className="app">
178      <h1>Enregistrement de l'équipage</h1>
179      <p className="subtitle">Validation de formulaire avec gestion des erreurs</p>
180      <CrewRegistrationForm />
181    </div>
182  );
183}

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quel est le principal avantage de la validation de formulaire côté client ?

  2. 2. Pourquoi utilisons-nous le modèle touched (onBlur) pour afficher les erreurs de validation ?

Tâches pratiques dans le jeu

  • Clique dans l'ordre

    Clique les éléments dans l'ordre pour les étapes du handleSubmit d'un formulaire :

  • Éditeur de code

    Implémente un formulaire avec validation et gestion des erreurs

  • Tri horizontal

    Ordonne la syntaxe correcte pour afficher conditionnellement une erreur de champ :

  • Tri vertical

    Ordonne les types d'erreurs de formulaire de la plus tôt gérée à la plus tardive :

Articles utiles