Erreurs critiques avalées : détection et gestion des échecs silencieux
Comprendre pourquoi intercepter une exception sans la traiter ni la propager détruit la cohérence du système, et comment concevoir une gestion d'erreur robuste sur les chemins critiques.
Sur cette page (12 sections)▼
Dans le développement applicatif, le réflexe d'enrober du code à risque dans un bloc try/catch est universel. Cependant, lorsque le bloc d'interception se contente d'ignorer l'erreur capturée (corps de catch vide, simple commentaire indicatif, ou promesse terminée par .catch(() => {})), le remède s'avère infiniment pire que le mal.
Ce comportement est qualifié d'erreur critique avalée (swallowed error ou silent catch).
Loin d'améliorer la stabilité de l'application, faire disparaître une exception sur un chemin critique empêche l'invalidation d'état, désactive les alertes de production et engendre des corruptions de données indétectables.
Le modèle mental : échec maîtrisé vs silence destructeur
Une application résiliente ne cherche pas à prétendre que les erreurs n'arrivent jamais. Elle s'assure qu'un échec sur une opération critique laisse une trace observable et interrompt la chaîne de causalité.
sequenceDiagram
participant User as Utilisateur / Client
participant Server as Serveur d'Application
participant DB as Base de Données
participant Stripe as Passerelle de Paiement
User->>Server: Valider la commande
Server->>Stripe: Débit de la carte (Succès)
Server->>DB: Insertion de la commande
Note over DB: Échec : Violation de contrainte SQL
Note over Server: catch (err) { /* vide */ }
Server-->>User: "Commande validée avec succès !"
Note over User,Server: Résultat : Client débité mais commande inexistante en base
Dans ce scénario possible, le serveur a « avalé » l'échec de persistance en base. La réponse HTTP émise vers le navigateur affiche une confirmation trompeuse. Le client est débité de son argent, mais son panier reste vide dans le système d'information. Retrouver l'origine du litige sans trace ni exception relève du cauchemar opérationnel.
Distinguer le critique de l'accessoire
Toutes les opérations d'une application n'ont pas la même importance vitale. La règle GVO-REL-SWALLOWED-CRITICAL-ERROR-V1 du moteur d'audit GVO opère une distinction nette entre deux familles de traitements :
1. Les opérations critiques (Interdiction formelle de catch vide)
- Les mutations en base de données (
insert,update,upsert,delete) ; - Les transactions financières et appels de facturation (Stripe, LemonSqueezy) ;
- L'émission de communications contractuelles (emails de confirmation, SMS de vérification) ;
- L'écriture de fichiers d'audit, de sécurité ou de conformité ;
- Les appels d'inférence d'IA dont dépend une décision métier subséquente.
Sur ces chemins, une exception doit impérativement déclencher soit une annulation (rollback), soit une alerte traçable, soit une propagation vers le gestionnaire global d'erreur.
2. Les opérations accessoires (Dégradation gracieuse tolérée)
- L'envoi de télémétrie ou de statistiques de tracking ;
- La purge de caches temporaires ;
- L'émission de logs secondaires en mode best-effort.
Sur ces opérations annexes, un échec ne doit pas bloquer la requête principale de l'utilisateur. Mais même dans ce cas, l'erreur doit idéalement faire l'objet d'un signalement d'observabilité discret sans impacter le retour utilisateur.
Les trois formes classiques de l'anti-pattern
Forme 1 : Le catch vide en bloc synchrone ou asynchrone
// ANTI-PATTERN 1 : LE CATCH LITTÉRALEMENT VIDE
try {
await db.transaction.commit();
} catch (error) {
// L'erreur disparaît corps et biens dans le néant
}
Forme 2 : Le commentaire passif en guise de traitement
// ANTI-PATTERN 2 : LE COMMENTAIRE TROMPEUR
try {
await stripe.subscriptions.cancel(subId);
} catch (e) {
// TODO: gérer l'erreur plus tard
}
Forme 3 : La promesse avec swallow délibéré
// ANTI-PATTERN 3 : L'ENCLUME ASYNCHRONE
persistAuditLog(auditData).catch(() => {});
Pattern recommandé : contextualiser, réparer ou propager
Pour traiter proprement les erreurs sur les chemins sensibles :
1. Propager avec contexte technique enrichi
Si la fonction courante n'est pas en mesure de corriger la situation, elle doit relancer l'exception en y adjoignant des métadonnées exploitables :
// PATTERN RECOMMANDÉ : PROPAGATION CONTEXTUALISÉE
export async function finalizeOrder(orderId: string, payload: OrderPayload) {
try {
return await db.orders.create({ data: payload });
} catch (error) {
// Journalisation exploitable avec identifiant technique stable
console.error('[COMMANDE_ECHEC] Impossible de finaliser la commande', {
orderId,
cause: error instanceof Error ? error.message : String(error),
});
// Relance de l'erreur pour interrompre la transaction appelante
throw new Error(`Échec de finalisation de la commande ${orderId}`);
}
}
2. Définir des codes de retour d'erreur explicites
Si votre architecture privilégie le modèle des résultats typés (Result<T, E>) plutôt que les exceptions :
// PATTERN TYPÉ : RETOUR EXPLICITE DE STATUT D'ÉCHEC
export async function transferFunds(fromId: string, toId: string, amount: number) {
try {
await executeBankTransfer(fromId, toId, amount);
return { success: true };
} catch (err) {
trackCriticalIncident('TRANSFER_FAILED', { fromId, toId, amount, err });
return {
success: false,
error: 'Virement impossible, aucun débit n\'a été effectué.'
};
}
}
Ce que GVO observe lors de l'audit
Le moteur d'audit de fiabilité GVO analyse structurellement vos blocs d'interception d'erreurs :
- Repérage des chemins sensibles : Le moteur cible les Server Actions, Route Handlers (
app/api/), webhooks, consumers et fonctions serveur. - Analyse des opérations enrobées : Identification des écritures ORM, appels Stripe, envois de mails ou mutations critiques logées dans le corps du
try. - Inspection linéaire du bloc catch : Détection des blocs où le flux de code entre le début du catch et sa fermeture ne contient aucune instruction exécutable (rejet, log contextuel, modification d'état ou retour explicite).
- Verdict de preuve :
CONFIRMED: Une opération critique est contenue dans untrydont lecatchcorrespondant est vide ou sans traitement.DISMISSED: L'opération est accessoire, ou le bloc catch implémente un traitement d'erreur observable.
À poursuivre
Vérifier ce point sur votre projet
GVO confronte le code et les configurations de votre application aux règles de sécurité réelles.