-
Notifications
You must be signed in to change notification settings - Fork 7
Gestion des erreurs fr FR
Résumé : Les erreurs sont inévitables. Cette page explique comment StartER les gère, et comment ajouter une gestion d'erreurs correcte à vos propres modules.
Ce que vous apprendrez :
- Identifier quelles erreurs sont gérées par les middleware vs. votre code
- Créer des erreurs avec des codes HTTP personnalisés
- Comprendre comment les error boundaries React fonctionnent dans StartER
Quand quelque chose échoue pendant une requête, StartER suit une chaîne claire :
Requête
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Chaîne de middlewares │
│ │
│ cookieParser → csrf → json → validateur → paramConverter │
│ │ │ │ │ │
│ │ │ 400 (body invalide) 404 (introuvable)|
│ │ 401 (jeton │
│ │ CSRF absent) │
│ ▼ │
│ Action │
│ │ │
│ ├── 200/201/204 (succès) │
│ └── throw Error ──────────────────────────┐ │
│ │ │
└─────────────────────────────────────────────────│───────────────┘
│
▼
┌────────────────┐
│ logErrors │
│ (console.error)│
└───────┬────────┘
│
▼
┌────────────────┐
│ sendErrors │
│ (réponse JSON) │
└────────────────┘
L'idée clé : la plupart des erreurs sont déjà gérées avant que votre action ne s'exécute. La chaîne de middlewares intercepte :
- 400 : corps de requête invalide (validateur)
- 401 : jeton CSRF absent (middleware csrf), token d'authentification invalide/absent (verifyAccessToken)
- 404 : ressource introuvable (param converter)
- 403 : problème d'autorisation (checkAccess dans les routes)
Votre action n'a besoin de gérer que les erreurs spécifiques à sa logique métier.
StartER utilise res.sendStatus() pour la gestion légère des statuts (qui envoie les messages texte bruts standard d'Express) et Zod pour les erreurs de validation structurées 400 Bad Request :
| Code de statut | Nom du statut | Méthode Express | Déclencheur / Middleware | Format du corps de réponse |
|---|---|---|---|---|
400 |
Bad Request | res.status(400).json(...) |
createValidator (échec de validation Zod) |
{ issues: ZodIssue[] } (objet JSON) |
401 |
Unauthorized | res.sendStatus(401) |
Middleware verifyAccessToken ou csrf
|
"Unauthorized" (texte brut) |
404 |
Not Found |
res.sendStatus(404) (ou 204 sur DELETE)
|
createParamConverter ou route API inconnue |
"Not Found" (texte brut) (ou corps vide pour 204)
|
409 (Optionnel)
|
Conflict | res.sendStatus(409) |
Action personnalisée (ex: gestion de doublon d'email) |
"Conflict" (texte brut) |
422 (Optionnel)
|
Unprocessable Entity | res.status(422).json(...) |
Règle métier optionnelle dans votre module | JSON ou texte brut personnalisé |
500 |
Internal Server Error | Middleware sendErrors
|
Gestionnaire d'erreurs global dans server.ts
|
Dev: { message, stack }Prod: { message: "Internal Server Error" }
|
En bas de server.ts, deux middlewares de gestion d'erreurs interceptent tout ce qui passe à travers :
// 1. Logger l'erreur pour le débogage
const logErrors: ErrorRequestHandler = (err, req, _res, next) => {
console.error(err);
console.error("on req:", req.method, req.path);
next(err);
};
// 2. Envoyer une réponse JSON structurée
const sendErrors: ErrorRequestHandler = (err, _req, res, _next) => {
const status = err.status ?? err.statusCode ?? 500;
res.status(status).json({
message: err.message ?? "Internal Server Error",
// La stack trace n'est exposée qu'en développement
...(isProduction ? {} : { stack: err.stack }),
});
};Cela signifie que toute erreur non interceptée dans votre action produira automatiquement une réponse 500 Internal Server Error en JSON. En développement, la stack trace est incluse pour faciliter le débogage. En production, elle est masquée.
La plupart du temps, vous n'avez pas besoin de le faire. Laissez les erreurs remonter jusqu'au gestionnaire global. Mais il y a des cas où vous devez intercepter :
Si une opération de base de données échoue à cause d'une contrainte métier (par ex. violation d'unicité), vous pouvez vouloir renvoyer un 409 Conflict au lieu d'un 500 générique :
const add: RequestHandler = (req, res) => {
try {
const insertId = groupRepository.create(req.body);
res.status(201).json({ insertId });
} catch (err) {
// SQLite lève une erreur sur les violations de contrainte UNIQUE
if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
res.status(409).json({ message: "Un groupe avec ce nom existe déjà" });
return;
}
// Relancer les erreurs inattendues vers le gestionnaire global
throw err;
}
};Important
Relancez toujours les erreurs que vous ne gérez pas explicitement. Avaler les erreurs silencieusement est la source la plus courante de bugs « impossibles ».
Si votre action effectue plusieurs opérations (par ex. créer un enregistrement et envoyer un email), vous pouvez avoir besoin d'intercepter l'échec de la seconde opération pour éviter un état partiel :
const register: RequestHandler = async (req, res) => {
const userId = userRepository.create(req.body);
try {
await sendWelcomeEmail(req.body.email);
} catch {
// L'email a échoué, mais l'utilisateur a été créé.
// Logger l'échec mais ne pas casser le flux.
console.error(`Échec de l'envoi de l'email de bienvenue à ${req.body.email}`);
}
res.status(201).json({ insertId: userId });
};Pour les opérations multi-tables, utilisez les transactions SQL pour garantir l'atomicité. Voir Many-to-many et transactions pour le pattern complet avec BEGIN, COMMIT, et ROLLBACK.
Express 5 supporte la convention err.status. Vous pouvez créer des erreurs avec des codes de statut spécifiques :
const edit: RequestHandler = (req, res) => {
const updated = itemRepository.update(req.item.id, req.body);
if (!updated) {
// La ligne existait quand le param converter a tourné,
// mais a été supprimée entre-temps (condition de concurrence).
const error = new Error("L'item a été supprimé pendant la requête");
(error as any).status = 410; // Gone
throw error;
}
res.sendStatus(204);
};Le gestionnaire global sendErrors lira err.status et l'utilisera comme code de statut HTTP.
StartER garde volontairement les réponses d'erreur concises :
| Statut | Signification | Quoi dire |
|---|---|---|
| 400 | Bad Request | Renvoyer les erreurs de validation Zod (sans risque : elles décrivent les champs attendus, pas l'état interne) |
| 401 | Unauthorized | Ne rien renvoyer (sendStatus(401)). Ne jamais révéler pourquoi l'authentification a échoué |
| 403 | Forbidden | Ne rien renvoyer (sendStatus(403)). Ne jamais révéler la règle de propriété |
| 404 | Not Found | Ne rien renvoyer (sendStatus(404)). Ne jamais confirmer si la ressource existe mais est inaccessible |
| 500 | Server Error | Renvoyer un message générique. Ne jamais exposer les stack traces, erreurs SQL ou chemins internes en production |
Warning
Les messages d'erreur détaillés sont un cadeau pour les attaquants. Une réponse 401 qui dit « Signature JWT invalide » indique à l'attaquant qu'il a un JWT et qu'il est presque valide. Un simple 401 ne lui dit rien.
Les sections précédentes couvrent les erreurs Express (API). Côté React, deux mécanismes gèrent les états de chargement et les échecs des composants.
Dans Layout.tsx, le <Outlet /> est enveloppé dans un élément <Suspense> :
<main>
<Suspense fallback={<p>Loading…</p>}>
<Outlet />
</Suspense>
</main>Quand un composant appelle use(getOrFetch("/api/something")), React suspend l'affichage du composant. L'élément <Suspense> intercepte cet état de suspension et affiche le fallback pendant le chargement des données. L'en-tête et la navigation restent visibles pendant ce temps.
Note
C'est une enveloppe unique au niveau du <Outlet>. Les composants individuels n'ont pas besoin de leur propre <Suspense>. Ajouter des éléments <Suspense> plus granulaires est possible mais pas nécessaire pour un projet starter.
Dans routes.tsx, toutes les routes enfants sont enveloppées dans une route sans chemin avec un errorElement :
children: [
{
// Route sans chemin : error boundary pour toutes les pages.
// React Router rend <Outlet /> par défaut (pas besoin de Component).
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Home /> },
// ...autres routes
],
},
]Quand un composant lève une erreur (par ex. getOrFetch("/api/something") échoue à cause d'une erreur réseau), cette error boundary l'intercepte et affiche ErrorPage à l'intérieur du Layout. La navigation reste visible et l'utilisateur peut revenir en arrière : pas de crash de page complet.
Note
La route racine a aussi son propre errorElement comme filet de sécurité ultime. Il intercepte les erreurs qui surviennent avant le rendu du Layout (par ex. échecs du loader). La route sans chemin gère les erreurs à l'intérieur du Layout.
- Laissez les middlewares faire le travail : les validateurs, param converters et middlewares d'authentification gèrent déjà 80% des scénarios d'erreur. Ne dupliquez pas leur logique dans vos actions.
- Interceptez spécifiquement, relancez génériquement : n'interceptez que les erreurs que vous savez gérer. Tout le reste doit atteindre le gestionnaire global.
-
Loggez les mutations : envisagez d'ajouter des
console.infopour les mutations réussies (POST, PUT, DELETE) afin de faciliter le débogage et l'audit. -
Testez les cas d'erreur : dans vos contrats, définissez toujours les scénarios
bad_request,unauthorized,forbiddenetnot_found. Ce sont les erreurs que vos utilisateurs rencontreront réellement.
Co-création IA
Bien démarrer
Explications
Guides
Référence
Aller plus loin