La gestion des erreurs est un aspect fondamental de toute API robuste. Une bonne stratégie d'erreur permet non seulement d'aider les développeurs front-end à comprendre ce qui ne va pas, mais aussi de protéger votre application en n'exposant pas d'informations sensibles. NestJS fournit un système puissant et flexible pour gérer les exceptions, basé sur des classes prédéfinies et des filtres personnalisables.
Dans ce tutoriel, nous allons explorer en profondeur la gestion des exceptions HTTP dans NestJS, en nous appuyant sur des exemples concrets tirés d'un projet réel : une plateforme de réservation de consultations de voyance.
Les exceptions HTTP intégrées de NestJS
NestJS expose une série d'exceptions prêtes à l'emploi, toutes héritant de la classe de base HttpException. Lorsque vous lancez l'une de ces exceptions, NestJS les intercepte automatiquement et renvoie une réponse HTTP correctement formatée avec le code de statut adéquat.
NotFoundException (404)
À utiliser lorsqu'une ressource demandée par l'utilisateur n'existe pas. C'est le cas typique d'une recherche par identifiant qui ne retourne aucun résultat.
async findOne(id: string): Promise<GuidanceService> {
const service = await this.serviceRepository.findOne({ where: { id } });
if (!service) {
throw new NotFoundException(`Service with ID "${id}" not found`);
}
return service;
}
Dans cet extrait du ServicesService, on retourne une 404 explicite si aucun service ne correspond à l'UUID fourni. Le client reçoit un message clair lui indiquant que la ressource n'existe pas.
BadRequestException (400)
Cette exception est destinée aux erreurs de validation métier ou aux données malformées envoyées par le client. Elle complète la validation automatique des DTO via les pipes (class-validator).
async create(createBookingDto: CreateBookingDto): Promise<Booking> {
const service = await this.servicesService.findOne(createBookingDto.serviceId);
if (service.type === ServiceType.APPOINTMENT) {
if (!createBookingDto.bookingDate || !createBookingDto.bookingTime) {
throw new BadRequestException(
'Date and time are required for appointment services',
);
}
const isAvailable = await this.isSlotAvailable(
createBookingDto.bookingDate,
createBookingDto.bookingTime,
);
if (!isAvailable) {
throw new BadRequestException('Selected time slot is not available');
}
}
if (service.type === ServiceType.WRITTEN && !createBookingDto.question) {
throw new BadRequestException(
'Question is required for written guidance services',
);
}
// ...
}
Ici, le service BookingService impose des règles métier différentes selon le type de prestation : un rendez-vous (téléphone/visio) exige une date et un créneau valides, tandis qu'une guidance écrite requiert une question. Chaque violation déclenche une BadRequestException avec un message explicite.
UnauthorizedException (401)
À lancer lorsqu'un utilisateur n'est pas authentifié, ou que ses identifiants sont invalides. Typiquement déclenchée par un JwtAuthGuard en cas de token manquant ou expiré.
ForbiddenException (403)
Pour les cas où l'utilisateur est authentifié mais ne dispose pas des droits suffisants, par exemple lorsqu'un utilisateur tente d'accéder à une route protégée par @Roles('admin') sans avoir ce rôle.
La distinction entre 401 et 403 est subtile mais importante : 401 = "qui es-tu ?", 403 = "je sais qui tu es, mais tu ne peux pas faire ça".
Le format de réponse d'erreur par défaut
Par défaut, NestJS retourne un objet JSON structuré comme suit :
{
"statusCode": 404,
"message": "Service with ID \"abc-123\" not found",
"error": "Not Found"
}
Ce format est cohérent et facile à consommer côté client. Cependant, dans une API de production, on souhaite souvent personnaliser ce format (ajouter un timestamp, le chemin de la requête, ou normaliser les champs).
Créer des exceptions personnalisées
Lorsque les exceptions intégrées ne suffisent pas, vous pouvez créer vos propres classes d'exception en étendant HttpException :
import { HttpException, HttpStatus } from '@nestjs/common';
export class SlotUnavailableException extends HttpException {
constructor(date: string, time: string) {
super(
{
statusCode: HttpStatus.CONFLICT,
message: `Le créneau ${time} du ${date} n'est plus disponible`,
error: 'Slot Unavailable',
code: 'SLOT_UNAVAILABLE',
},
HttpStatus.CONFLICT,
);
}
}
L'avantage est double : vous centralisez le message d'erreur et vous pouvez ajouter un code métier que le front-end pourra utiliser pour afficher une UI adaptée (par exemple proposer un autre créneau).
Créer un ExceptionFilter personnalisé
Pour aller plus loin, NestJS permet de créer des filtres d'exception qui interceptent toutes les erreurs et les transforment avant l'envoi au client. C'est particulièrement utile pour :
- Uniformiser le format de réponse
- Logger les erreurs côté serveur
- Masquer les détails techniques en production
- Ajouter des métadonnées (timestamp, chemin, méthode)
Voici un filtre global complet utilisé dans le projet site-voyance :
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name);
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message: string | string[] = 'Une erreur interne est survenue';
let error = 'Internal Server Error';
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
if (typeof exceptionResponse === 'object' && exceptionResponse !== null) {
const res = exceptionResponse as Record<string, any>;
message = res.message || exception.message;
error = res.error || 'Error';
} else {
message = exception.message;
}
}
// Log complet côté serveur
if (status >= 500) {
this.logger.error(
`${request.method} ${request.url} → ${status}`,
exception instanceof Error ? exception.stack : String(exception),
);
} else if (status >= 400) {
this.logger.warn(
`${request.method} ${request.url} → ${status}: ${JSON.stringify(message)}`,
);
}
// Réponse au client : pas de stack trace en production
const responseBody: Record<string, any> = {
statusCode: status,
message,
error,
};
if (process.env.NODE_ENV === 'production') {
if (status >= 500) {
responseBody.message = 'Une erreur interne est survenue';
}
if (status === 401) {
responseBody.message = 'Non autorisé';
}
if (status === 403) {
responseBody.message = 'Accès refusé';
}
}
response.status(status).json(responseBody);
}
}
Plusieurs points méritent attention dans ce filtre :
- Le décorateur
@Catch()sans argument capture toutes les exceptions, qu'elles soient HTTP ou non (erreurs de base de données, exceptions JavaScript, etc.). - Les erreurs
5xxsont loggées avec leur stack trace vialogger.error(), tandis que les4xxsont en simplewarn(ce sont des erreurs attendues, dues au client). - En production (
NODE_ENV === 'production'), les messages techniques sont remplacés par des messages génériques pour ne pas exposer la structure interne (par exemple "Une erreur interne est survenue" au lieu de "Cannot read property 'id' of undefined").
Appliquer un filtre globalement ou localement
Application globale
Pour qu'un filtre s'applique à toutes les routes de l'application, on l'enregistre dans le fichier main.ts via useGlobalFilters :
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new AllExceptionsFilter());
await app.listen(3000);
}
bootstrap();
C'est l'approche recommandée pour un filtre transverse comme la journalisation et la gestion d'erreurs centralisée.
Application locale avec @UseFilters
Pour des cas spécifiques, vous pouvez appliquer un filtre uniquement à un contrôleur ou à une route via le décorateur @UseFilters() :
import { Controller, Post, Body, UseFilters } from '@nestjs/common';
import { BookingExceptionFilter } from './booking-exception.filter';
@Controller('bookings')
@UseFilters(BookingExceptionFilter)
export class BookingController {
@Post()
create(@Body() dto: CreateBookingDto) {
return this.bookingService.create(dto);
}
}
Logger les erreurs sans bloquer le flux
Parfois, vous voulez logger une erreur sans pour autant interrompre la requête principale. C'est typiquement le cas pour les actions secondaires comme l'envoi d'un email de confirmation :
if (status === BookingStatus.CONFIRMED && !booking.confirmationSent) {
// ...
try {
await this.emailService.sendBookingConfirmation(customerEmail, { /* ... */ });
booking.confirmationSent = true;
} catch (error) {
this.logger.error('Failed to send confirmation emails:', error.message);
}
}
Si l'envoi d'email échoue, on log l'erreur mais la réservation reste confirmée. C'est un bon pattern pour les opérations "best effort" qui ne doivent pas faire échouer la transaction principale.
Bonnes pratiques et pièges à éviter
- Ne jamais exposer les stack traces en production. Elles révèlent la structure interne du code et facilitent les attaques.
- Utilisez le bon code HTTP. Une mauvaise donnée envoyée par le client = 400, pas 500. Un conflit (créneau déjà pris) = 409. Une ressource introuvable = 404.
- Centralisez les messages. Évitez de dupliquer les chaînes de caractères dans toute l'application en utilisant des constantes ou des classes d'exception personnalisées.
- Loggez avec contexte. Incluez la méthode HTTP, l'URL, et idéalement l'ID utilisateur pour faciliter le debugging.
- Distinguez les erreurs attendues des erreurs inattendues. Une 404 est attendue, une 500 ne devrait jamais l'être.
Conclusion
La gestion des erreurs dans NestJS repose sur trois piliers : les exceptions HTTP intégrées pour signaler clairement les problèmes, les exceptions personnalisées pour modéliser votre logique métier, et les ExceptionFilter globaux pour uniformiser les réponses et protéger votre application. En combinant ces outils avec une stratégie de journalisation rigoureuse, vous obtenez une API fiable, facile à déboguer et sécurisée.
Pour aller plus loin, vous pouvez explorer la documentation officielle des Exception Filters, intégrer un outil de monitoring comme Sentry ou Datadog pour centraliser vos logs en production, ou encore créer des filtres dédiés à des modules spécifiques (par exemple un filtre pour transformer les erreurs TypeORM en exceptions HTTP appropriées).

Commentaires