Modèles de Gestion d'Erreur
Modèles de gestion d'erreur cohérents et robustes pour les applications de production.
Quand Activer
- Conception de hiérarchies d'erreurs ou d'exceptions pour un nouveau module ou service
- Ajout de logique de reprise ou d'interrupteurs de circuit pour des dépendances externes non fiables
- Révision de points de terminaison API pour détecter des erreurs de gestion manquantes
- Implémentation de messages d'erreur et de retours utilisateur
- Débogage de défaillances en cascade ou d'erreurs silencieuses avalées
Principes Fondamentaux
- Échouer vite et bruyamment — faire remonter les erreurs à la frontière où elles se produisent ; ne pas les enterrer
- Erreurs typées plutôt que messages chaînes — les erreurs sont des valeurs de première classe avec une structure
- Messages utilisateur ≠ messages développeur — afficher du texte convivial aux utilisateurs, enregistrer le contexte complet côté serveur
- Ne jamais avaler les erreurs silencieusement — chaque bloc
catchdoit soit traiter, relancer ou enregistrer - Les erreurs font partie de votre contrat API — documenter chaque code d'erreur qu'un client peut recevoir
TypeScript / JavaScript
Classes d'Erreur Typées
// Définir une hiérarchie d'erreurs pour votre domaine
export class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number = 500,
public readonly details?: unknown,
) {
super(message)
this.name = this.constructor.name
// Maintenir une chaîne de prototype correcte dans le JavaScript ES5 transpilé.
// Requis pour que les vérifications `instanceof` (par ex., `error instanceof NotFoundError`)
// fonctionnent correctement lors de l'extension de la classe Error intégrée.
Object.setPrototypeOf(this, new.target.prototype)
}
}
export class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(`${resource} introuvable : ${id}`, 'NOT_FOUND', 404)
}
}
export class ValidationError extends AppError {
constructor(message: string, details: { field: string; message: string }[]) {
super(message, 'VALIDATION_ERROR', 422, details)
}
}
export class UnauthorizedError extends AppError {
constructor(reason = 'Authentification requise') {
super(reason, 'UNAUTHORIZED', 401)
}
}
export class RateLimitError extends AppError {
constructor(public readonly retryAfterMs: number) {
super('Limite de débit dépassée', 'RATE_LIMITED', 429)
}
}
Modèle de Résultat (style sans levée d'exception)
Pour les opérations où l'échec est attendu et courant (analyse syntaxique, appels externes) :
type Result<T, E = AppError> =
| { ok: true; value: T }
| { ok: false; error: E }
function ok<T>(value: T): Result<T> {
return { ok: true, value }
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error }
}
// Utilisation
async function fetchUser(id: string): Promise<Result<User>> {
try {
const user = await db.users.findUnique({ where: { id } })
if (!user) return err(new NotFoundError('Utilisateur', id))
return ok(user)
} catch (e) {
return err(new AppError('Erreur base de données', 'DB_ERROR'))
}
}
const result = await fetchUser('abc-123')
if (!result.ok) {
// TypeScript connaît ici result.error
logger.error('Échec de récupération de l'utilisateur', { error: result.error })
return
}
// TypeScript connaît ici result.value
console.log(result.value.email)
Gestionnaire d'Erreur API (Next.js / Express)
import { NextRequest, NextResponse } from 'next/server'
function handleApiError(error: unknown): NextResponse {
// Erreur d'application connue
if (error instanceof AppError) {
return NextResponse.json(
{
error: {
code: error.code,
message: error.message,
...(error.details ? { details: error.details } : {}),
},
},
{ status: error.statusCode },
)
}
// Erreur de validation Zod
if (error instanceof z.ZodError) {
return NextResponse.json(
{
error: {
code: 'VALIDATION_ERROR',
message: 'Échec de la validation de la requête',
details: error.issues.map(i => ({
field: i.path.join('.'),
message: i.message,
})),
},
},
{ status: 422 },
)
}
// Erreur inattendue — enregistrer les détails, renvoyer un message générique
console.error('Erreur inattendue :', error)
return NextResponse.json(
{ error: { code: 'INTERNAL_ERROR', message: 'Une erreur inattendue s\'est produite' } },
{ status: 500 },
)
}
export async function POST(req: NextRequest) {
try {
// ... logique du gestionnaire
} catch (error) {
return handleApiError(error)
}
}
Limite d'Erreur React
import { Component, ErrorInfo, ReactNode } from 'react'
interface Props {
fallback: ReactNode
onError?: (error: Error, info: ErrorInfo) => void
children: ReactNode
}
interface State {
hasError: boolean
error: Error | null
}
export 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) {
this.props.onError?.(error, info)
console.error('Erreur React non gérée :', error, info)
}
render() {
if (this.state.hasError) return this.props.fallback
return this.props.children
}
}
// Utilisation
<ErrorBoundary fallback={<p>Quelque chose s\'est mal passé. Veuillez actualiser.</p>}>
<MyComponent />
</ErrorBoundary>
Python
Hiérarchie d'Exceptions Personnalisées
class AppError(Exception):
"""Erreur d'application de base."""
def __init__(self, message: str, code: str, status_code: int = 500):
super().__init__(message)
self.code = code
self.status_code = status_code
class NotFoundError(AppError):
def __init__(self, resource: str, id: str):
super().__init__(f"{resource} introuvable : {id}", "NOT_FOUND", 404)
class ValidationError(AppError):
def __init__(self, message: str, details: list[dict] | None = None):
super().__init__(message, "VALIDATION_ERROR", 422)
self.details = details or []
Gestionnaire d'Exception Global FastAPI
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError) -> JSONResponse:
return JSONResponse(
status_code=exc.status_code,
content={"error": {"code": exc.code, "message": str(exc)}},
)
@app.exception_handler(Exception)
async def generic_error_handler(request: Request, exc: Exception) -> JSONResponse:
# Enregistrer tous les détails, renvoyer un message générique
logger.exception("Erreur inattendue", exc_info=exc)
return JSONResponse(
status_code=500,
content={"error": {"code": "INTERNAL_ERROR", "message": "Une erreur inattendue s'est produite"}},
)
Go
Erreurs Sentinel et Encapsulation d'Erreurs
package domain
import "errors"
// Erreurs sentinelles pour la vérification de type
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrConflict = errors.New("conflict")
)
// Encapsuler les erreurs avec contexte — ne jamais perdre l'original
func (r *UserRepository) FindByID(ctx context.Context, id string) (*User, error) {
user, err := r.db.QueryRow(ctx, "SELECT * FROM users WHERE id = $1", id)
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("utilisateur %s : %w", id, ErrNotFound)
}
if err != nil {
return nil, fmt.Errorf("interrogation de l'utilisateur %s : %w", id, err)
}
return user, nil
}
// Au niveau du gestionnaire, déballer pour déterminer la réponse
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
user, err := h.service.GetUser(r.Context(), chi.URLParam(r, "id"))
if err != nil {
switch {
case errors.Is(err, domain.ErrNotFound):
writeError(w, http.StatusNotFound, "not_found", err.Error())
case errors.Is(err, domain.ErrUnauthorized):
writeError(w, http.StatusForbidden, "forbidden", "Accès refusé")
default:
slog.Error("erreur inattendue", "err", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Une erreur inattendue s'est produite")
}
return
}
writeJSON(w, http.StatusOK, user)
}
Tenter avec Backoff Exponentiel
interface RetryOptions {
maxAttempts?: number
baseDelayMs?: number
maxDelayMs?: number
retryIf?: (error: unknown) => boolean
}
async function withRetry<T>(
fn: () => Promise<T>,
options: RetryOptions = {},
): Promise<T> {
const {
maxAttempts = 3,
baseDelayMs = 500,
maxDelayMs = 10_000,
retryIf = () => true,
} = options
let lastError: unknown
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn()
} catch (error) {
lastError = error
if (attempt === maxAttempts || !retryIf(error)) throw error
const jitter = Math.random() * baseDelayMs
const delay = Math.min(baseDelayMs * 2 ** (attempt - 1) + jitter, maxDelayMs)
await new Promise(resolve => setTimeout(resolve, delay))
}
}
throw lastError
}
// Utilisation : tenter les erreurs réseau transitoires, pas les 4xx
const data = await withRetry(() => fetch('/api/data').then(r => r.json()), {
maxAttempts: 3,
retryIf: (error) => !(error instanceof AppError && error.statusCode < 500),
})
Messages d'Erreur Destinés à l'Utilisateur
Mapper les codes d'erreur en messages lisibles par l'homme. Garder les détails techniques hors du texte visible par l'utilisateur.
const USER_ERROR_MESSAGES: Record<string, string> = {
NOT_FOUND: 'L\'élément demandé est introuvable.',
UNAUTHORIZED: 'Veuillez vous connecter pour continuer.',
FORBIDDEN: 'Vous n\'avez pas la permission de faire cela.',
VALIDATION_ERROR: 'Veuillez vérifier votre saisie et réessayer.',
RATE_LIMITED: 'Trop de requêtes. Veuillez patienter un moment et réessayer.',
INTERNAL_ERROR: 'Un problème est survenu de notre côté. Veuillez réessayer plus tard.',
}
export function getUserMessage(code: string): string {
return USER_ERROR_MESSAGES[code] ?? USER_ERROR_MESSAGES.INTERNAL_ERROR
}
Checklist de Gestion d'Erreur
Avant de fusionner tout code qui touche à la gestion des erreurs :
- Chaque bloc
catchtraite, relance ou enregistre — pas d'erreur avalée silencieusement - Les erreurs API suivent l'enveloppe standard
{ error: { code, message } } - Les messages destinés à l'utilisateur ne contiennent ni traces de pile ni détails internes
- Le contexte complet de l'erreur est enregistré côté serveur
- Les classes d'erreur personnalisées étendent une classe de base
AppErroravec un champcode - Les fonctions asynchrones font remonter les erreurs aux appelants — pas de "fire-and-forget" sans solution de repli
- La logique de reprise ne tente que les erreurs récupérables (pas les erreurs client 4xx)
- Les composants React sont encapsulés dans
ErrorBoundarypour les erreurs de rendu