Clean code et refactoring - 06 - Gestion des erreurs propre : fail fast, fail loud

Les erreurs silencieuses sont les pires. Fail fast, classes d'erreurs custom, error boundaries React : guide pratique de la gestion d'erreurs.

  1. 01 Clean code et refactoring - 00 - Pourquoi le clean code est un investissement, pas un luxe
  2. 02 Clean code et refactoring - 01 - Nommage : la competence la plus sous-estimee
  3. 03 Clean code et refactoring - 02 - Fonctions : courtes, claires, responsables
  4. 04 Clean code et refactoring - 03 - Conditions et lisibilité : sortir de la pyramide
  5. 05 Clean code et refactoring - 04 - Commentaires et documentation : quand le code ne suffit pas
  6. 06 Clean code et refactoring - 05 - Immutabilite et effets de bord : moins de surprises, moins de bugs
  7. 07 Clean code et refactoring - 06 - Gestion des erreurs propre : fail fast, fail loud
  8. 08 Clean code et refactoring - 07 - Programmation defensive vs offensive : valider aux frontieres, faire confiance a l'intérieur
  9. 09 Clean code et refactoring - 08 - SOLID en pratique avec TypeScript
  10. 10 Clean code et refactoring - 09 - DRY, KISS, YAGNI
  11. 11 Clean code et refactoring - 10 - Couplage et cohesion
  12. 12 Clean code et refactoring - 11 - Complexite cyclomatique
  13. 13 Clean code et refactoring - 12 - Abstractions prematurees vs tardives
  14. 14 Clean code et refactoring - 13 - Code smells
  15. 15 Clean code et refactoring - 14 - Techniques de refactoring
  16. 16 Clean code et refactoring - 15 - Refactoring legacy sans tout casser
  17. 17 Clean code et refactoring - 16 - Tests comme filet de sécurité pour le refactoring
  18. 18 Clean code et refactoring - 17 - Structurer un projet — feature-based vs layer-based
  19. 19 Clean code et refactoring - 18 - Constantes, configuration et magic numbers
  20. 20 Clean code et refactoring - 19 - Linting et formatting — ESLint, Biome, automatiser la qualité
  21. 21 Clean code et refactoring - 20 - Conventions d'équipe et ADR
  22. 22 Clean code et refactoring - 21 - Dette technique — quand elle est acceptable, quand elle tue le projet
  23. 23 Clean code et refactoring - 22 - Code review — donner et recevoir du feedback
  24. 24 Clean code et refactoring - 23 - Glossaire — tous les termes de la serie

06 - Gestion des erreurs propre : fail fast, fail loud

Ce que tu vas apprendre

  • Pourquoi les erreurs silencieuses sont les pires bugs
  • Le principe fail fast : échouer tot, échouer fort
  • Comment créer des classes d'erreurs custom en TypeScript
  • Les types d'erreurs : validation, métier, infrastructure
  • Pourquoi ne jamais attraper et ignorer une erreur
  • Les error boundaries en React
  • La différence entre erreurs développeur et erreurs utilisateur

Prerequisites

05 - Immutabilite et effets de bord


Un client m'a appele un vendredi soir. Son site e-commerce ne prenait plus de commandes depuis trois heures. Pas d'alerte. Pas d'erreur dans les logs. Le site fonctionnait normalement en apparence. L'utilisateur cliquait "Commander", le spinner tournait, et... rien. Pas de message d'erreur. Pas de confirmation. Juste rien.

Le coupable etait ce code dans le service de paiement :

typescriptasync function processPayment(order: Order): Promise<void> {
  try {
    await stripe.charges.create({
      amount: order.total,
      currency: "eur",
      source: order.paymentToken,
    });
  } catch (error) {
    // TODO: handle this properly
    console.log(error);
  }
}

catch (error) { console.log(error); }. L'erreur etait attrapee, loggee dans la console du serveur (que personne ne regardait un vendredi soir), et avalee. Le code continuait comme si tout allait bien. Le token Stripe avait expire, mais l'application ne le savait pas.

Trois heures de commandes perdues. Parce que quelqu'un a écrit un catch vide avec un TODO.

Les erreurs silencieuses : le pire des bugs

Une erreur qui crashe l'application, c'est desagreable mais c'est detectable. Une erreur silencieuse, c'est un bug qui corrompt les donnees pendant des semaines avant que quelqu'un ne le remarque.

Les patterns silencieux a eliminer :

typescript// Retourner null sans explication
function findUser(id: string): User | null {
  try {
    return db.users.findById(id);
  } catch {
    return null; // erreur DB ? mauvais ID ? timeout ? on ne saura jamais
  }
}

// Retourner un tableau vide
function getOrders(userId: string): Order[] {
  try {
    return db.orders.findByUser(userId);
  } catch {
    return []; // l'utilisateur pense qu'il n'a pas de commandes
  }
}

// Retourner une valeur par defaut
function getConfig(key: string): string {
  try {
    return configService.get(key);
  } catch {
    return ""; // la config est cassee mais l'app tourne avec des valeurs vides
  }
}

Dans chaque cas, l'appelant ne sait pas qu'une erreur s'est produite. Il continue avec des donnees fausses.

Fail fast : échouer tot, échouer fort

Le principe est simple : quand quelque chose va mal, arrêté-toi immédiatement et dis-le clairement. Ne continue pas avec des donnees corrompues.

typescript// Mauvais : continue avec des donnees potentiellement corrompues
function processOrder(order: Order): OrderResult {
  const user = findUser(order.userId); // peut retourner null
  const items = getItems(order.itemIds); // peut retourner []

  // On continue meme si user est null et items est vide
  const total = items.reduce((sum, i) => sum + i.price, 0);
  return { user, total, items };
}

// Bon : echoue immediatement si quelque chose manque
function processOrder(order: Order): OrderResult {
  const user = findUser(order.userId);
  if (!user) throw new NotFoundError(`User ${order.userId} not found`);

  const items = getItems(order.itemIds);
  if (items.length === 0) throw new ValidationError("Order has no items");

  const total = items.reduce((sum, i) => sum + i.price, 0);
  return { user, total, items };
}

Fail fast et early returns (article sur les conditions) fonctionnent ensemble. Les guard clauses sont du fail fast applique.

Classes d'erreurs custom

Les erreurs natives de JavaScript (Error, TypeError, RangeError) ne portent pas assez de contexte. Cree tes propres classes :

typescriptclass AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly statusCode: number = 500,
    public readonly isOperational: boolean = true
  ) {
    super(message);
    this.name = this.constructor.name;
  }
}

class ValidationError extends AppError {
  constructor(message: string) {
    super(message, "VALIDATION_ERROR", 400);
  }
}

class NotFoundError extends AppError {
  constructor(resource: string) {
    super(`${resource} not found`, "NOT_FOUND", 404);
  }
}

class BusinessError extends AppError {
  constructor(message: string) {
    super(message, "BUSINESS_ERROR", 422);
  }
}

class InfrastructureError extends AppError {
  constructor(message: string, public readonly cause?: Error) {
    super(message, "INFRA_ERROR", 503, false);
  }
}

Pourquoi isOperational ? Pour distinguer les erreurs attendues (validation, not found, regles métier) des erreurs inattendues (base de donnees en panne, disque plein). Les erreurs operationnelles se gerent proprement. Les erreurs non-operationnelles necessitent une intervention humaine.

Les trois categories d'erreurs

Validation : les donnees en entree sont invalides.

typescriptfunction createAccount(input: CreateAccountInput): Account {
  if (!input.email.includes("@")) {
    throw new ValidationError("Email invalide");
  }
  if (input.password.length < 8) {
    throw new ValidationError("Mot de passe trop court (min 8 caracteres)");
  }
  // ...
}

Métier : les regles métier sont violees.

typescriptfunction withdrawMoney(account: Account, amount: number): Transaction {
  if (amount > account.balance) {
    throw new BusinessError("Solde insuffisant");
  }
  if (amount > account.dailyLimit) {
    throw new BusinessError("Limite journaliere depassee");
  }
  // ...
}

Infrastructure : un système externe est en panne.

typescriptasync function fetchUserFromDB(id: string): Promise<User> {
  try {
    return await db.users.findById(id);
  } catch (error) {
    throw new InfrastructureError(
      `Impossible de lire l'utilisateur ${id}`,
      error instanceof Error ? error : new Error(String(error))
    );
  }
}

Cette categorisation aide a la gestion. J'utilise ces distinctions dans tous mes projets. J'en parle aussi sur paltemps.fr dans le contexte des architectures en couches.

Ne jamais attraper et ignorer

La regle la plus simple et la plus violee :

typescript// INTERDIT
try {
  await riskyOperation();
} catch (error) {
  // silencieux
}

// INTERDIT
try {
  await riskyOperation();
} catch (error) {
  console.log(error); // log que personne ne lit
}

// ACCEPTABLE : re-throw apres enrichissement
try {
  await riskyOperation();
} catch (error) {
  throw new InfrastructureError("riskyOperation failed", error as Error);
}

// ACCEPTABLE : gestion explicite et complete
try {
  await riskyOperation();
} catch (error) {
  if (error instanceof NotFoundError) {
    return createDefaultResource();
  }
  throw error; // re-throw ce qu'on ne sait pas gerer
}

Si tu attrapes une erreur, tu dois soit la gerer complètement, soit la re-throw (eventuellement enrichie). Jamais l'avaler.

Error boundaries en React

React 18+ fournit les error boundaries pour attraper les erreurs de rendu :

typescriptimport { Component, type ErrorInfo, type ReactNode } from "react";

type Props = { children: ReactNode; fallback: ReactNode };
type State = { hasError: boolean; error: Error | null };

class ErrorBoundary extends Component<Props, State> {
  state: State = { hasError: false, error: null };

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, info: ErrorInfo): void {
    // Envoie a ton service de monitoring (Sentry, DataDog, etc.)
    errorReporting.capture(error, { componentStack: info.componentStack });
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

// Utilisation
function App() {
  return (
    <ErrorBoundary fallback={<ErrorPage />}>
      <Dashboard />
    </ErrorBoundary>
  );
}

Place tes error boundaries de manière stratégique. Un boundary global pour le crash fatal. Des boundaries locaux pour les sections independantes (sidebar, widget, formulaire).

Erreurs utilisateur vs erreurs développeur

L'utilisateur ne doit jamais voir un stack trace ou un code technique. Deux messages distincts :

typescriptfunction handleApiError(error: unknown): ApiResponse {
  if (error instanceof ValidationError) {
    return {
      status: error.statusCode,
      // Message pour l'utilisateur - clair, actionnable
      userMessage: error.message,
      // Code pour le developpeur front - pour le switch/case
      errorCode: error.code,
    };
  }

  if (error instanceof BusinessError) {
    return {
      status: error.statusCode,
      userMessage: error.message,
      errorCode: error.code,
    };
  }

  // Erreur inattendue - ne jamais exposer les details
  logger.error("Unhandled error", { error });
  return {
    status: 500,
    userMessage: "Une erreur est survenue. Veuillez reessayer.",
    errorCode: "INTERNAL_ERROR",
  };
}

Le logger.error pour le développeur. Le userMessage pour l'utilisateur. Jamais l'inverse.

Logger proprement

Un bon log d'erreur contient du contexte :

typescript// Mauvais
logger.error(error.message);

// Bon
logger.error("Payment processing failed", {
  orderId: order.id,
  userId: user.id,
  amount: order.total,
  paymentProvider: "stripe",
  error: {
    name: error.name,
    message: error.message,
    stack: error.stack,
  },
});

Le contexte permet de reproduire le problème. Sans le orderId et le userId, tu ne peux pas retrouver le cas precis dans la base de donnees.

Résumé

  • Les erreurs silencieuses (return null, catch vide) sont les pires bugs
  • Fail fast : echoue immédiatement quand quelque chose ne va pas
  • Cree des classes d'erreurs custom avec un code, un status HTTP, et un flag operationnel
  • Trois categories : validation, métier, infrastructure
  • Ne jamais attraper une erreur sans la gerer ou la re-throw
  • Erreurs utilisateur : message clair et actionnable. Erreurs développeur : contexte riche pour le debug.
  • Les error boundaries React attrapent les erreurs de rendu par zone

Article précédent : 05 - Immutabilite et effets de bord

Article suivant : 07 - Programmation defensive vs offensive

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.