Aller au contenu

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.

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 backtrace

Le 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.

▶ Essayez par vous-même

À 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).

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));
}
▶ Essayez par vous-même
fn main() {
let value: Option<i32> = None;
println!("{}", value.unwrap()); // panique : called `Option::unwrap()` on a `None` value
}
▶ Essayez par vous-même
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));
}
▶ Essayez par vous-même

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.

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 Result
fn 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 : panic
fn 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}");
}
▶ Essayez par vous-même

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 est false.
  • 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 de match pour 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 : 80
le jour 3 est mercredi
▶ Essayez par vous-même

Ici, 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: 80

Ce 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.

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 8080
▶ Essayez par vous-même

É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 30
▶ Essayez par vous-même
  • panic! arrête immédiatement le programme avec un message d’erreur.
  • Certaines opérations standard (un indice de tableau invalide, .unwrap() sur None, une division par zéro) paniquent automatiquement.
  • Utilisez panic! pour les bugs et les invariants qui devraient être impossibles ; utilisez Result pour 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! et unreachable! paniquent volontairement quand une chose que vous pensiez vraie se révèle fausse.

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