Panic en Rust
Une panic (panique), c’est la façon qu’a Rust d’arrêter immédiatement un programme quand quelque chose a mal tourné, sans possibilité de s’en remettre. Si vous avez suivi les pages précédentes, vous en avez sans doute déjà provoqué quelques-unes sans le vouloir : lire au-delà de la fin d’un tableau, ou appeler .unwrap() sur un None, déclenche une panic.
Déclencher une panic à la main
Section intitulée « Déclencher une panic à la main »fn main() { panic!("quelque chose a très mal tourné");}thread 'main' (12345) panicked at src/main.rs:2:5:quelque chose a très mal tournénote: run with `RUST_BACKTRACE=1` environment variable to display a backtraceLe nombre entre parenthèses, ici (12345), est l’identifiant du thread. Il change à chaque exécution, et les versions plus anciennes de Rust ne l’affichent pas.
À l’exécution, le programme affiche ce message et s’arrête : rien de ce qui suit le panic! n’est exécuté. Les lignes autour de votre message sont écrites par Rust, en anglais : elles indiquent le thread (main), le fichier, la ligne et la colonne où la panic s’est produite, et la note explique comment afficher la pile d’appels (backtrace).
Ce qui panique sans que vous appeliez panic!
Section intitulée « Ce qui panique sans que vous appeliez panic! »Certaines opérations que vous connaissez déjà paniquent d’elles-mêmes si quelque chose ne va pas. Dans chaque exemple ci-dessous, la valeur risquée arrive par un paramètre de fonction plutôt que d’être écrite en dur. Avec un indice ou un 0 écrit directement dans le code, le compilateur peut souvent comprendre dès la compilation que l’opération échouera toujours, et il refuse alors de compiler au lieu de laisser le programme tourner et paniquer.
fn get(numbers: &[i32], index: usize) -> i32 { numbers[index] // panique : index out of bounds (indice hors limites)}
fn main() { let numbers = [1, 2, 3]; println!("{}", get(&numbers, 10));}fn main() { let value: Option<i32> = None; println!("{}", value.unwrap()); // panique : called `Option::unwrap()` on a `None` value}fn divide(a: i32, b: i32) -> i32 { a / b // panique : attempt to divide by zero (tentative de division par zéro)}
fn main() { let a = 10; let b = 0; println!("{}", divide(a, b));}Dans chacun de ces cas, Rust refuse de laisser votre programme continuer dans un état qui n’a plus de sens, plutôt que de renvoyer discrètement n’importe quoi.
panic! ou Result ?
Section intitulée « panic! ou Result ? »Voici la vraie décision que vous prendrez souvent : cet échec doit-il faire planter le programme, ou l’appelant doit-il avoir une chance de le gérer ?
panic!: pour les bugs et les situations qui ne devraient jamais arriver si le code est correct. Il n’existe aucune façon raisonnable de s’en remettre, donc s’arrêter immédiatement (avant de causer d’autres dégâts) est l’option la plus sûre.Result(présenté sur la page précédente) : pour les échecs qu’un appelant peut raisonnablement prévoir et vouloir gérer, comme une saisie invalide, un fichier manquant ou une requête réseau qui échoue.
// Une saisie invalide de l'utilisateur : on peut s'en remettre, donc Resultfn parse_age(input: &str) -> Result<u32, String> { input.parse().map_err(|_| String::from("âge invalide"))}
// Un invariant qui devrait être impossible si le code est correct : panicfn get_first(numbers: &[i32]) -> i32 { if numbers.is_empty() { panic!("get_first appelée avec une slice vide"); } numbers[0]}unwrap() et expect() sont des raccourcis vers la panic
Section intitulée « unwrap() et expect() sont des raccourcis vers la panic »Vous avez vu .unwrap() sur Option comme sur Result : ce n’est en fait qu’un raccourci pour « donne-moi la valeur, ou panique s’il n’y en a pas ». .expect("message") fait la même chose, mais vous permet d’y joindre un message plus utile :
fn main() { let input = "42"; let number: i32 = input.parse().expect("l'entrée devrait être un nombre valide"); println!("{number}");}Si la conversion échoue, le message de la panic contient votre texte, ce qui facilite beaucoup le débogage par rapport au message générique de .unwrap().
Vérifier vos hypothèses : assert!, assert_eq! et unreachable!
Section intitulée « Vérifier vos hypothèses : assert!, assert_eq! et unreachable! »Parfois, vous savez qu’une chose doit être vraie à un certain endroit de votre code. Rust propose quelques petites macros qui transforment cette certitude en vérification. Si la vérification échoue, elles paniquent avec un message clair : le bug apparaît ainsi là où il commence, au lieu de produire des résultats étranges plus tard.
assert!(condition, "message")panique si la condition estfalse.assert_eq!(left, right)panique si les deux valeurs ne sont pas égales, et les affiche toutes les deux.unreachable!("message")marque du code qui ne devrait jamais s’exécuter, comme une branche dematchpour une valeur impossible.
fn average(numbers: &[i32]) -> i32 { assert!(!numbers.is_empty(), "average a besoin d'au moins un nombre");
let mut total = 0; for n in numbers { total += n; } total / numbers.len() as i32}
fn day_name(day: u8) -> &'static str { match day { 1 => "lundi", 2 => "mardi", 3 => "mercredi", 4 => "jeudi", 5 => "vendredi", 6 => "samedi", 7 => "dimanche", _ => unreachable!("le jour doit être entre 1 et 7, reçu {day}"), }}
fn main() { let avg = average(&[70, 80, 90]); assert_eq!(avg, 80); println!("moyenne : {avg}");
println!("le jour 3 est {}", day_name(3));}Sortie :
moyenne : 80le jour 3 est mercrediIci, toutes les vérifications passent, donc le programme s’exécute normalement. Maintenant, remplacez la liste par [70, 80, 95] dans le Playground. La moyenne devient 81, et assert_eq! arrête le programme en affichant les deux valeurs côte à côte :
thread 'main' (12345) panicked at src/main.rs:26:5:assertion `left == right` failed left: 81 right: 80Ce message signifie que l’assertion left == right a échoué : la valeur de gauche (left) vaut 81, celle de droite (right) vaut 80.
Essayez aussi average(&[]) ou day_name(9) : vous verrez vos propres messages dans la panic.
Ces vérifications s’exécutent toujours, même dans une compilation release : gardez-les donc peu coûteuses. Vous retrouverez assert_eq! sur la page Les tests, où c’est le moyen principal pour un test de dire « ceci doit être égal à cela ». Il existe aussi todo!(), qui panique avec le message « not yet implemented » (pas encore implémenté). C’est pratique comme corps provisoire, pendant que vous esquissez une fonction que vous terminerez plus tard.
Exercices
Section intitulée « Exercices »1. Un numéro de port avec expect
Section intitulée « 1. Un numéro de port avec expect »Vous avez une valeur de configuration let port_text = "8080";. Transformez-la en u16 avec .parse() et .expect(...), en utilisant un message qui explique ce qui ne va pas. Affichez ensuite Écoute sur le port 8080. Indice : la variable a besoin d’un type, let port: u16 = ..., pour que parse sache quoi produire. Ensuite, remplacez le texte par "80a0" pour voir votre message dans la panic.
Voir la solution
fn main() { let port_text = "8080"; let port: u16 = port_text .parse() .expect("le port de la configuration devrait être un nombre de 0 à 65535"); println!("Écoute sur le port {port}");}Sortie :
Écoute sur le port 80802. Partager l’addition
Section intitulée « 2. Partager l’addition »Écrivez fn split_bill(total: u32, people: u32) -> u32, qui renvoie la part de chaque personne. Partager entre zéro personne est un bug dans le code appelant : utilisez donc assert! avec un message clair pour le repérer. Affichez la part pour un total de 120 et 4 personnes. Indice : placez le assert! sur la première ligne de la fonction, avant la division.
Voir la solution
fn split_bill(total: u32, people: u32) -> u32 { assert!(people > 0, "split_bill a besoin d'au moins une personne"); total / people}
fn main() { let share = split_bill(120, 4); println!("Chaque personne paie {share}");}Sortie :
Chaque personne paie 30Récapitulatif
Section intitulée « Récapitulatif »panic!arrête immédiatement le programme avec un message d’erreur.- Certaines opérations standard (un indice de tableau invalide,
.unwrap()surNone, une division par zéro) paniquent automatiquement. - Utilisez
panic!pour les bugs et les invariants qui devraient être impossibles ; utilisezResultpour les échecs qu’un appelant peut raisonnablement gérer. .expect("message")fonctionne comme.unwrap(), mais avec un message qui rend la panic plus facile à déboguer.assert!,assert_eq!etunreachable!paniquent volontairement quand une chose que vous pensiez vraie se révèle fausse.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Que fait un appel à panic! ?
2. D'après la règle générale de cette page, quand faut-il utiliser panic! plutôt que Result ?
3. Quelle est la différence entre .unwrap() et .expect('message') ?
Score : 0 / 3
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Unrecoverable Errors with panic! — The Rust Book (en anglais) : ce qui se passe pendant une panic, le déroulement de la pile (unwinding) et comment lire une backtrace.
- To panic! or Not to panic! — The Rust Book (en anglais) : les recommandations officielles pour choisir entre
panic!etResult, avec des exemples. - panic! — Rust by Example (en anglais) : un court exemple de panic, qui montre que la mémoire est quand même libérée pendant le déroulement de la pile.
assert_eq!— documentation std (en anglais) : comment fonctionnent les macros d’assertion, y compris avec des messages personnalisés.panic!— documentation std (en anglais) : la référence depanic!lui-même, son lien avecunwrap, et quand une panic est le bon outil.