1. Découverte de la pile d'appels (Call Stack)
Dans la leçon précédente, on a vite fait parlé de la pile d'appels, maintenant on va creuser un peu plus.
Imagine : un utilisateur clique sur un bouton dans l'appli. Ce bouton déclenche la méthode OnClick() ("Bouton cliqué"). Celle-ci appelle à son tour LoadData() (chargement des données), qui elle-même appelle ReadFromFile() (lecture depuis un fichier).
Et là, dans ReadFromFile(), bim, une erreur — fichier introuvable. Qui est responsable ?
Pour piger ce qui se passe, le programme remonte la piste : de ReadFromFile() → à LoadData() → à OnClick(). Ce chemin, c'est ce qu'on appelle la pile d'appels — comme une pile d'assiettes, celle du dessus tombe en premier.
Le programme descend cette pile jusqu'à trouver un catch qui va bien, mais il exécute tous les finally sur le chemin — histoire que tout soit bien fermé, libéré et rangé.
En dev, la pile d'appels c'est une liste qui se souvient qui a appelé qui, pour que tu puisses remonter le "chemin des appels" et trouver la cause du bug.
Comment ça marche
Quand le programme tourne, chaque appel de méthode (ou fonction) ajoute une nouvelle "ligne" dans la pile (la liste) d'appels. Si une exception se produit, la classe .NET Exception garde l'info sur cette pile : où et dans quel ordre les méthodes ont été appelées avant le bug.
2. À quoi sert la pile d'appels
La pile d'appels, c'est ton meilleur pote pour déboguer les bugs bien relous.
- Elle te montre pas seulement ce qui s'est passé, mais aussi où exactement et qui est le coupable.
- Parfois, en matant la pile, tu vas halluciner de voir comment ton programme a pu arriver là (surtout si quelqu'un a passé une valeur d'argument chelou par erreur).
Histoire classique : T'as un gros projet, les méthodes s'appellent les unes les autres sur dix niveaux. À un moment, paf, NullReferenceException, mais tu piges pas comment le programme est arrivé là. Tu ouvres la pile — tu vois toute la chaîne d'appels, et là c'est beaucoup plus simple de savoir où commencer à chercher.
Exemple :
class MyClass
{
public void MethodA() { MethodB(); }
public void MethodB() { MethodC(); }
public void MethodC() {
throw new Exception("Erreur !");
}
public void Main()
{
try
{
MethodA();
}
catch (Exception ex)
{
Console.WriteLine(ex.StackTrace);
}
}
}
3. Comment créer tes propres exceptions
Parfois les exceptions standards ne suffisent pas
En .NET, t'as plein d'exceptions standards (ArgumentNullException, InvalidOperationException, etc.), mais parfois ça ne suffit pas :
- Dans ton appli, t'as tes propres "règles du jeu" : genre, un utilisateur ne peut pas acheter plus de 10 articles d'un coup, ou dans ta logique métier il ne doit pas y avoir de sommes négatives.
- Tu veux séparer les bugs de logique métier des bugs "système".
C'est là que t'as envie de créer "ta propre" exception — ben ouais, tant qu'à faire !
using System;
// Exception custom : utilisateur non trouvé
public class UserNotFoundException : Exception
{
// constructeur de base
public UserNotFoundException() : base("Utilisateur non trouvé.") { }
// constructeur avec message
public UserNotFoundException(string message) : base(message) { }
// constructeur avec message et exception interne
public UserNotFoundException(string message, Exception inner) : base(message, inner) { }
}
Petit topo sur les constructeurs
- Sans paramètres — ça met un message standard
- Avec ton propre message — parfois tu veux filer plus de détails
- Avec une exception interne (inner) — comme ça, si l'erreur vient "d'une autre erreur", tu perds pas l'info importante.
Comment utiliser ta propre exception
Imaginons qu'on modélise une méthode de recherche d'utilisateur dans notre appli de gestion de tâches :
using System;
public class UserService
{
public string FindUserNameById(int userId)
{
// On "cherche" l'utilisateur, si pas trouvé — on balance une exception
if (userId != 42)
throw new UserNotFoundException($"Utilisateur avec id {userId} non trouvé.");
return "Maxime";
}
}
Dans le programme principal :
// Dans Main
var service = new UserService();
try
{
string name = service.FindUserNameById(17);
Console.WriteLine("Nom de l'utilisateur : " + name);
}
catch (UserNotFoundException ex)
{
Console.WriteLine("Problème de recherche utilisateur : " + ex.Message);
// La pile d'appels est aussi dispo via ex.StackTrace
}
Si on passe un id différent de 42, on obtient :
Problème de recherche utilisateur : Utilisateur avec id 17 non trouvé.
4. Pourquoi créer tes propres exceptions
Logging et séparation des bugs
Imaginons, dans ton appli t'as plein de bugs différents, et tu dois les gérer différemment. Par exemple, les bugs de base de données tu les loggues comme "fatals", les bugs utilisateurs — tu affiches un message rouge à l'utilisateur, et les bugs réseau — tu retentes l'opération. Grouper par type d'exception, c'est top pour ça.
POO et héritage
Tu peux organiser une hiérarchie d'exceptions pour ton domaine métier :
public class MyAppException : Exception { ... }
public class OrderException : MyAppException { ... }
public class ProductException : MyAppException { ... }
public class TooManyItemsInOrderException : OrderException { ... }
Maintenant, si tu attrapes MyAppException, tu gères tous les bugs de ton domaine, et si tu veux une réaction spéciale pour "commande trop grosse" — tu attrapes le cas précis.
5. À retenir quand tu crées tes propres exceptions
- Ne balance pas des exceptions juste pour le fun
Crée tes propres exceptions seulement si :- Ça rend vraiment le code plus clair
- Il y a une chance qu'elles soient attrapées dans le code appelant
- Tu veux filer plus d'infos au code appelant (via des champs/propriétés)
- Bonne pratique : la sérialisation
En .NET, les exceptions standards supportent la sérialisation (pour envoyer sur le réseau, par exemple). Dans les applis simples, t'en as rarement besoin, mais pour les cas "avancés" — ajoute l'attribut [Serializable] et fais un constructeur pour la sérialisation (voir la doc officielle). Pour les exemples d'apprentissage, pas besoin, mais au taf — demande à ton lead :)
La sérialisation, c'est le processus de transformer un objet en un format pratique pour le stockage ou l'envoi (genre dans un fichier ou sur le réseau). On verra la sérialisation plus en détail plus tard.
6. Particularités de la pile d'appels : où ça peut devenir confus
La pile d'appels montre juste le chemin jusqu'au point où l'exception s'est produite. Si tu attrapes une exception et que tu en relances une nouvelle sans préciser "l'exception interne" (le constructeur Exception(string, Exception inner)), tu risques de perdre la source du bug. On appelle ça "cacher la pile".
Pas cool :
try
{
// Ici, une erreur
}
catch (Exception ex)
{
throw new Exception("Une erreur inconnue est survenue."); // La pile de l'erreur précédente est perdue !
}
Mieux :
try
{
// Ici, une erreur
}
catch (Exception ex)
{
throw new Exception("Une erreur inconnue est survenue.", ex); // On garde la pile originale !
}
Comme ça, dans StackTrace, tu gardes le chemin de la première erreur et ton nouveau message.
7. Conseils pratiques et erreurs fréquentes
- Évite de balancer des exceptions pour gérer la logique normale (genre pour "sortir d'une boucle" — y a des moyens plus propres !).
- Attrape le bon type d'exception — attraper tout le temps Exception c'est pas toujours une bonne idée (tu risques de "gober" un bug que tu devrais pas).
- Log toujours la pile d'appels pour les bugs compliqués. Ça te sauvera la vie pour trouver les bugs.
- Utilise les exceptions internes — ça aide à pas perdre l'info sur la cause première du bug.
- Décris tes exceptions clairement — pour que celui qui lit le code dans un an pige pourquoi ce bug est arrivé.
GO TO FULL VERSION