ProgrammationProgrammeur système

Expliquez comment la gestion des erreurs se fait à travers Result<T, E> et comment utiliser correctement l'opérateur point d'interrogation (?), sans compromettre la sécurité et la lisibilité du code.

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

Rust est construit sur une gestion explicite des erreurs : les exceptions sont absentes, à la place, on utilise la valeur de retour Result<T, E>. Cela garantit la sécurité et la prévisibilité du code.

Contexte :

De nombreux langages ont emprunté la voie des exceptions, ce qui a conduit à des situations inattendues et à la nécessité de gérer explicitement les exceptions en cours d'exécution. Rust a, dès le début, misé sur le contrôle par le biais des types, toutes les erreurs doivent faire partie de la signature de la fonction.

Problème :

Le principal défi est d'écrire du code dans lequel les erreurs ne sont pas ignorées ou masquées, il n'y a pas de fonctions "qui plantent", mais le code reste compact et lisible. Il est nécessaire de propager correctement les erreurs vers le haut, sans perdre d'informations sur leur type, et sans compliquer la logique.

Solution :

L'outil clé est le type Result<T, E> et l'opérateur ?, qui "déroule" automatiquement le résultat : en cas d'erreur, la fonction sort immédiatement avec le retour de l'erreur, et en cas de succès, la valeur est renvoyée.

Exemple de code :

fn read_number(file: &str) -> Result<i32, std::io::Error> { let content = std::fs::read_to_string(file)?; let num: i32 = content.trim().parse()?; Ok(num) }

Caractéristiques clés :

  • Déclaration explicite de toutes les erreurs potentielles dans la signature
  • L'opérateur ? permet de simplifier l'imbrication (supprime if-let/unwrap/expect)
  • Les erreurs peuvent être facilement "enveloppées" ou transformées (via .map_err() et d'autres méthodes)

Questions piégeuses.

Peut-on utiliser ? dans des fonctions retournant Unit (void) ?

Non, l'opérateur ? n'est autorisé que dans des fonctions retournant Result ou Option. Si la fonction retourne (), il n'est pas possible d'utiliser ?.

Que se passe-t-il si les types d'erreurs dans plusieurs appels avec ? sont différents ?

Cela entraîne une erreur de compilation : le type d'erreur doit être clairement défini. Il faut soit convertir toutes les erreurs en un seul type via .map_err(), soit utiliser thiserror, ou bien décrire une enveloppe enum au niveau de l'API. Exemple :

fn foo() -> Result<_, MyError> { let a = bar()?; let b = baz().map_err(MyError::Baz)?; Ok() }

Pourquoi .unwrap() est-il dangereux dans la logique interne ?

Il est courant de croire à tort que l'on peut utiliser unwrap() sans problème dans le code "principal", "il ne va certainement pas planter". En réalité, même une petite erreur invisible provoquera une panique à l'exécution — ce qui compromettra la sécurité du programme.

Erreurs typiques et anti-patterns

  • Intercepter toutes les erreurs grâce à unwrap/expect, surtout dans la logique interne
  • Perte de contexte lors de map_err(|_| …), lorsque l'erreur est englobée sans conserver les informations d'origine
  • Imbrication excessive lors du traitement manuel de chaque erreur sans ?

Exemple concret

Cas négatif

Dans le code de production, beaucoup de unwrap() ont été laissés après un débogage rapide. En conséquence, l'application plantait à chaque échec de parsing en raison d'une entrée utilisateur incorrecte.

Avantages :

  • Débogage rapide

Inconvénients :

  • Risque potentiel de crash de l'application, difficile de trouver la cause

Cas positif

Un usage complet de Result<T, E> avec description explicite des erreurs et n'utilisant que ? et .map_err(), en préservant toutes les informations sur l'échec.

Avantages :

  • Facilité de débogage, comportement prévisible

Inconvénients :

  • Un peu plus de types d'erreurs "bruyants"