Pourquoi les erreurs sont inévitables
Tout programme finit par rencontrer une situation imprévue : un serveur renvoie une erreur 500, un utilisateur saisit des lettres dans un champ numérique, un fichier est manquant ou une API tierce change de structure. La gestion des erreurs ne consiste pas à empêcher chaque panne, mais à décider de ce qui se passe lorsqu’elle survient.
Des erreurs bien gérées permettent à une application de continuer à fonctionner, informent l’utilisateur de manière utile et fournissent aux développeurs les détails nécessaires. À l’inverse, des erreurs mal gérées produisent des écrans blancs, des pertes de données silencieuses et des rapports de bugs impossibles à reproduire.
L’objet Error
JavaScript possède un type Error intégré, et chaque erreur levée est généralement une instance de celui-ci.
// error.js
const error = new Error("Something broke");
error.message; // "Something broke"
error.name; // "Error"
error.stack; // the call stack at the point of creation
error.cause; // an optional underlying error
Il existe plusieurs sous-classes intégrées pour les catégories courantes : TypeError, ReferenceError, RangeError, SyntaxError, URIError et EvalError. Elles diffèrent principalement par leur nom et les situations qui les génèrent, ce qui les rend utiles pour le débogage et pour adapter le comportement du code selon le type d’échec.
Lever des erreurs
Utilisez throw pour signaler qu’une règle a été enfreinte. Levez un objet Error, jamais une chaîne de caractères ou un nombre — seules les instances Error transportent un message, un nom et une trace de pile (stack trace).
// validate.js
function divide(a, b) {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
Lever une erreur précocement, exactement au point où une hypothèse n’est plus vérifiée, empêche un état invalide de se propager plus profondément dans votre programme. Échouer bruyamment durant le développement est une fonctionnalité, pas une nuisance.
try, catch et finally
try exécute du code risqué. Si une erreur est levée, le contrôle passe à catch. finally s’exécute dans tous les cas, ce qui en fait l’endroit idéal pour le nettoyage.
// try.js
try {
const data = JSON.parse(input);
save(data);
} catch (error) {
console.error("Invalid data:", error.message);
} finally {
setLoading(false);
}
Le paramètre catch est la valeur levée. Le JavaScript moderne supporte également la liaison catch optionnelle (optional catch binding) lorsque vous n’en avez pas besoin : catch { ... }. À l’intérieur d’un bloc catch, vous pouvez inspecter l’erreur, l’enregistrer dans les logs, tenter de récupérer l’état ou la relancer si vous ne pouvez pas la gérer.
// rethrow.js
try {
await loadProfile();
} catch (error) {
log(error);
throw error; // let a higher layer decide
}
Classes d’erreur personnalisées
Les erreurs génériques perdent de leur sens à mesure que votre application s’agrandit. Les classes personnalisées vous permettent d’ajouter du contexte et de gérer la logique en fonction du type d’échec.
// http-error.js
class HttpError extends Error {
constructor(status, message) {
super(message ?? `HTTP ${status}`);
this.name = "HttpError";
this.status = status;
}
}
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
Désormais, les appelants peuvent réagir avec précision :
// handle.js
try {
await save(form);
} catch (error) {
if (error instanceof ValidationError) {
showFieldError(error.field, error.message);
} else if (error instanceof HttpError && error.status === 401) {
redirectToLogin();
} else {
showGenericError();
}
}
C’est toute la différence entre « une erreur est survenue » et « le champ email est déjà utilisé ».
Envelopper les erreurs avec cause
Parfois, vous souhaitez ajouter du contexte sans perdre l’échec d’origine. L’option cause permet de préserver la chaîne.
// cause.js
try {
await db.query(sql);
} catch (error) {
throw new Error("Failed to load orders", { cause: error });
}
L’erreur externe contient le contexte compréhensible par l’utilisateur, tandis que error.cause conserve les détails d’origine pour les logs et le débogage.
Erreurs dans le code asynchrone
Le code asynchrone présente deux chemins d’échec supplémentaires. Avec async/await, try/catch fonctionne exactement comme pour le code synchrone.
// async.js
async function load() {
try {
const res = await fetch("/api/data");
if (!res.ok) throw new HttpError(res.status);
return await res.json();
} catch (error) {
console.error("Load failed:", error);
throw error;
}
}
Sans await, attachez un gestionnaire à la promesse :
// promise.js
fetch("/api/data")
.then((res) => res.json())
.catch((error) => console.error(error));
Une promesse rejetée sans gestionnaire devient un unhandled rejection. Dans les navigateurs, cela génère un avertissement ; dans Node.js, cela peut interrompre le processus. Gérez toujours les rejets, ne serait-ce que pour les journaliser et les relancer.
Filets de sécurité globaux
Même avec un code rigoureux, certains problèmes finiront par passer entre les mailles du filet. Les gestionnaires globaux capturent ce que vous avez manqué afin que l’application puisse le signaler au lieu de planter silencieusement.
// global.js
window.addEventListener("error", (event) => {
reportToServer(event.error);
});
window.addEventListener("unhandledrejection", (event) => {
reportToServer(event.reason);
});
Dans Node.js, les équivalents sont process.on("uncaughtException") et process.on("unhandledRejection"). Considérez-les comme des derniers recours pour le logging et l’arrêt progressif (graceful shutdown), et non comme un substitut à la gestion des erreurs là où elles surviennent.
Validez avant de lever une exception
Tous les problèmes ne sont pas exceptionnels. Les conditions prévisibles — une entrée vide, un champ optionnel manquant, une recherche sans résultat — sont mieux gérées par la validation que par des exceptions.
// guards.js
function formatName(user) {
if (!user?.name) return "Anonymous";
return user.name.trim();
}
Réservez les exceptions aux situations réellement inattendues ou irrécupérables. Les utiliser pour le flux de contrôle ordinaire rend le code plus lent et plus difficile à suivre.
Communiquer avec les utilisateurs
Un message d’erreur technique est destiné aux développeurs. Les utilisateurs, quant à eux, doivent savoir ce qui s’est produit et quelle action ils peuvent entreprendre.
- Gardez des messages courts et humains : « Nous n’avons pas pu enregistrer vos modifications. Vérifiez votre connexion et réessayez. »
- Évitez le jargon, les stack traces et les codes de statut bruts dans l’interface.
- Proposez une étape suivante : réessayer, revenir en arrière, contacter le support ou continuer hors ligne.
- Journalisez l’intégralité des détails — message, stack, contexte — afin que le support puisse mener l’enquête.
Bonnes pratiques
- Levez des objets
Error, jamais des chaînes de caractères. - Ne capturez les erreurs que là où vous pouvez les résoudre ou ajouter un contexte utile.
- Utilisez des classes d’erreur personnalisées pour les échecs spécifiques à votre domaine.
- Enveloppez les erreurs avec
causeau lieu de supprimer l’originale. - Gérez systématiquement les rejets de promesses.
- Ajoutez des gestionnaires globaux comme dernier filet de sécurité, et envoyez les logs vers un service dédié.
- Validez les conditions attendues plutôt que de lever des erreurs.
- Séparez les messages destinés aux utilisateurs des diagnostics destinés aux développeurs.
- Écrivez des tests pour vos scénarios d’échec, et pas seulement pour le “happy path”.
Erreurs courantes
- Ignorer les erreurs avec un bloc
catchvide. - Lever des chaînes de caractères et perdre ainsi la trace de la pile (stack trace).
- Capturer une erreur et retourner une valeur par défaut qui masque un bug réel.
- Oublier de gérer les promesses rejetées.
- Afficher des messages d’erreur bruts aux utilisateurs.
- Utiliser des exceptions pour le flux de contrôle ordinaire.
- Supposer que
try/catchautour d’un exécuteur de promesse capture les erreurs asynchrones — ce n’est pas le cas.
Et après ?
Les erreurs sont le point de rencontre entre le code asynchrone, l’Fetch API et la réalité. Associez les patterns présentés ici au DOM pour afficher des états conviviaux, et n’hésitez pas à revoir les fondamentaux de JavaScript dès qu’un TypeError vous rappelle que undefined n’est pas une fonction.