Gérer ses erreurs en Rust
Lecture 13 min. âą
Table des matiĂšres
- RĂ©trospective de la gestion dâerreurs
- Contexte dâerreur
- Qualifier sa chaĂźne dâerreurs
- Eyre est lĂ pour nous faciliter la vie
- Utiliser eyre dans un exemple concret
- Utiliser eyre dans le contexte dâune lib
- Conclusion
Bonjour Ă toutes et tous đ
Les erreurs en Rust sont un vaste sujet.
Au fur et Ă mesure que lâapplication se complexifie, la gestion des erreurs devient cruciale.
Le problĂšme est que chaque bibliothĂšque expose son propre systĂšme dâerreurs.
Si votre projet utilise un certain nombre de bibiliothĂšques, vous pouvez vous trouver dans la situation oĂč vous devez rĂ©aliser des adaptations de code pour permettre aux erreurs dâĂȘtre compatibles entre-elles.
Avant de se plonger dans la crate eyre que je souhaite vous prĂ©senter, nous allons faire un petit tour dâhorizon de ce qui existe dĂ©jĂ .
RĂ©trospective de la gestion dâerreurs
La situation dâurgence : le panic
Il existe en Rust une maniĂšre dâinterrompre une exĂ©cution. Il sâagit du mĂ©canisme de panic.
fn will_fail() {
panic!("Ce programme meurt ici et maintenant")
}
fn main() {
will_fail();
println!("Ne sera jamais affiché");
}LâidĂ©e est simple, si le code arrive dans une instruction de panic, lâexĂ©cution se stoppe immĂ©diatement et le programme retourne la stack trace.
thread 'main' panicked at 'Ce programme meurt ici et maintenant', src\main.rs:2:5
stack backtrace:
2: error_reporting::will_fail
at .\src\main.rs:2
3: error_reporting::main
at .\src\main.rs:6
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
error: process didn't exit successfully: `target\debug\error-reporting.exe` (exit code: 101)Ce mĂ©canisme permet de prĂ©server un systĂšme dâun Ă©tat qui ne peut pas ĂȘtre gĂ©rĂ© et qui doit provoquer son extinction pur et simple.
Exemple
Le fichier de configuration nâest pas trouvable et il nây a pas de valeur par dĂ©faut.
Le gros point faible de ce systĂšme, est quâune fois quâun programme a paniquĂ©, il est difficile (mais pas impossible) de le rĂ©cupĂ©rer.
Information
Si vous voulez effectuer une rĂ©cupĂ©ration dâun panic, il existe un mĂ©thode
catch_unwind, qui permet de faire poursuivre le programme en capturant la panique.fn main() { catch_unwind(will_fail); println!("Hello world"); }Par contre, ce systĂšme nâest pas conçu pour vous permettre de faire un mĂ©canisme de
try/catch. Il est uniquement là pour assurer la compatibilité avec des libs en C par exemple.
Utilisation dâun boolĂ©en comme status de retour
La rĂ©ponse naĂŻve dans la gestion dâerreur consiste Ă renvoyer un boolĂ©en true/false.
Il indique lâĂ©tat dâun traitement.
Généralement:
- true : tout sâest bien passĂ©
- false : une erreur est survenue
fn process() -> bool {
true
}
fn main() {
if process() {
println!("SuccĂšs")
} else {
println!("Ăchec")
}
}Utilisation dâun code de retour numĂ©rique
Ce systĂšme marche bien lorsquâil nây a que deux Ă©tats possibles.
Sinon lâon doit faire comme en C oĂč lâon utilise des codes de retour.
- 0 : tout câest bien passĂ©
- 1 : une erreur recouvrable est survenue
- autre chose : une erreur impossible à résoudre est survenue
fn process() -> u8 {
2
}
fn main() {
match process() {
0 => println!("SuccĂšs"),
1 => println!("Erreur"),
_ => panic!("Crash"),
}
}Ce systĂšme rudimentaire, permet de pouvoir gĂ©rer plus finement le dĂ©roulĂ© de notre programme. Mais ce nâest pas encore suffisant.
La gestion idiomatique des erreurs en Rust
Rust possĂšde son propre systĂšme de gestion dâerreur basĂ© sur une Ă©numĂ©ration Result.
Celle-ci se compose ainsi:
enum Result<T, E> {
Ok(T),
Err(E)
}Les deux variantes de lâĂ©numĂ©ration Ă©tant gĂ©nĂ©riques. Le type de la valeur de retour et le type de lâerreur ne sont pas contraints.
Nous allons crĂ©er notre propre erreur au travers dâune enumĂ©ration:
enum MyError {
Error,
Failure,
}Nous alors utiliser lâobjet Rusult comme retour de notre mĂ©thode process.
fn process(input: i8) -> Result<u8, MyError> {
if input < 0 {
return Result::Err(MyError::Failure);
}
if input > 10 {
return Result::Err(MyError::Error);
}
Result::Ok(input + 1)
}Puis traiter lâerreur Ă©ventuelle, au travers dâun match.
fn main() {
match process(41) {
Ok(val) => {
println!("SuccĂšs");
println!("val : {val}");
}
Err(MyError::Error) => {
println!("Erreur le nombre est supérieur à 10");
}
Err(MyError::Failure) => panic!("Le nombre ne peut pas ĂȘtre nĂ©gatif"),
};Utiliser cette énumération nous ouvre certaines possibilitées comme la propagation des erreurs.
Propagation dâerreur
Imaginons quâune erreur soit levĂ©e dans un appel profond de notre programme.
fn deep() -> Result<(), MyError> {
Err(MyError::Error)
}
fn process(input: u8) -> Result<u8, MyError> {
let result = deep();
println!("After deep");
Ok(input + 1)
}Si lâon exĂ©cute la fonction process, nous allons voir sâafficher :
After deep`Or, nous ne voulons pas ça.
Nous voulons que lâexĂ©cution de process se stoppe si la mĂ©thode deep est en erreur.
Pour ce faire, nous devons propager lâerreur venant de la mĂ©thode deep.
fn process(input: u8) -> Result<u8, MyError> {
let result = deep();
if let Err(error) = result {
return Err(error);
}
println!("After deep");
Ok(input + 1)
}AprĂšs cette modification nous nâavons plus le After deep dans la console. Nous avons rĂ©alisĂ© un early return de notre erreur.
Un ou deux sucre avec votre erreur ?
Le fait dâutiliser un Result de la stdlib nous ouvre aussi la porte Ă lâutilisation dâune syntaxe particuliĂšre qui permet de propager une erreur.
Celle-ci consiste Ă rajouter un ? Ă la fin de lâappel dâune fonction retournant un Result.
Ceci provoquera le retour immĂ©diat de lâerreur si celle-ci survient.
fn process(input: u8) -> Result<u8, MyError> {
deep()?;
println!("After deep");
Ok(input + 1)
}Bien sĂ»r, ce processus peut se rĂ©aliser Ă nâimporte quelle profondeur dâexĂ©cution.
fn below_deep() -> Result<(), MyError> {
Err(MyError::Error)
}
fn deep() -> Result<(), MyError> {
below_deep()?;
Ok(())
}
fn process(input: u8) -> Result<u8, MyError> {
deep()?;
println!("After deep");
Ok(input + 1)
}Ce qui permet de se concentrer sur lâessentiel câest Ă dire le dĂ©roulĂ© du code lorsquâil y a pas dâĂ©chec.
Contexte dâerreur
Savoir que le code est en erreur est une chose, savoir pourquoi il lâest, câest encore mieux.
Pour cela, nous allons aggrĂ©menter notre erreur dâun contexte qui nous permettra de mieux cerner le souci.
enum MyError {
Error(String),
Failure,
}Au lieu de renvoyer une simple erreur, nous allons Ă©galement rajouter du contexte sous la forme dâune chaĂźne de caractĂšres.
fn below_deep() -> Result<(), MyError> {
Err(MyError::Error(
"occurs into below_deep function".to_string(),
))
}
fn deep() -> Result<(), MyError> {
below_deep()?;
Err(MyError::Error("occurs into deep function".to_string()))
}Puis, nous pouvons alors afficher cette Erreur avec plus de détails.
fn main() {
let val = match process(old_val) {
Ok(val) => {
println!("SuccĂšs");
println!("val : {val}")
}
Err(MyError::Error(details)) => {
println!("Erreur: {details}");
}
Err(MyError::Failure) => panic!("Crash"),
};
}Cela nous donne dans la console:
Erreur: occurs into below_deep functionNous sommes dĂ©sormais, dans la capacitĂ© de dĂ©terminer quâune erreur sâest produite, et pourquoi elle sâest produite.
Mais, nous ne savons pas oĂč elle sâest produite.
Pour cela nous allons devoir agrémenter un peu notre code.
Capturer la backtrace dans le contexte
Il existe une crate qui se nomme backtrace-rs qui est capable dâeffectuer ce travail pour nous.
Dâabord, nous installons la crate.
cargo add backtracePuis, nous allons ensuite modifier notre structure dâerreur.
Ensuite, nous transformons la variante Error en une structure possédant deux champs:
- details qui contient les dĂ©tails de lâerreur
- backtrace la callstack amenant Ă lâerreur
enum MyError {
Error {
backtrace: Backtrace,
details: String,
},
Failure,
}Pour nous faciliter la vie nous créons un contructeur
impl MyError {
fn new(details: &str) -> Self {
let backtrace = Backtrace::new();
MyError::Error {
backtrace,
details: details.to_string(),
}
}
}Que nous utilisons dans la suite du code
fn below_deep() -> Result<(), MyError> {
Err(MyError::new("occurs into below deep function"))
}
fn deep() -> Result<(), MyError> {
below_deep()?;
Err(MyError::new("occurs into deep function"))
}On modifie Ă©galement la façon dâafficher les erreurs, pour rajouter la notion de backtrace.
fn main() {
let val = match process(old_val) {
Ok(val) => {
println!("SuccĂšs");
println!("val : {val}");
}
Err(MyError::Error { details, backtrace }) => {
println!("Erreur: {details} \n {backtrace:?}");
}
Err(MyError::Failure) => panic!("Crash"),
};
println!("val : {val}")
}Ce qui nous donne Ă lâexĂ©cution lâerreur suivante:
Erreur: occurs into below deep function
5: enum$<error_reporting::MyError, 1, 18446744073709551615, Error>::new
at src\main.rs:13
6: error_reporting::below_deep
at src\main.rs:32
7: error_reporting::deep
at src\main.rs:36
8: error_reporting::process
at src\main.rs:41
9: error_reporting::main
at src\main.rs:49Si on dĂ©crit cette stacktrace sous la forme dâun graph cela nous donne:
flowchart LR
main --> process --> deep --> below_deep -.-> MyError::new
Sauf que le MyError::new est de trop dans lâhistoire, il vient polluer la stacktrace avec une Ă©tape inutile.
Pour éviter cet appel à new, nous allons utiliser une macro qui va effectuer le tavail à notre place.
Si vous avez un trou de mĂ©moire ou que ne connaissez pas le principe des macros, jâai rĂ©diger un article dessus. đ
macro_rules! raise {
($details:expr) => {
Err(MyError::Error {
backtrace: Backtrace::new(),
details: $details.to_string(),
})
};
}Que lâon utilise ainsi:
fn below_deep() -> Result<(), MyError> {
raise!("occurs into below deep function")
}
fn deep() -> Result<(), MyError> {
below_deep()?;
raise!("occurs into deep function")
}Cette fois-ci la stacktrace devient:
Erreur: occurs into below deep function
5: error_reporting::below_deep
at src\main.rs:21
6: error_reporting::deep
at src\main.rs:25
7: error_reporting::process
at src\main.rs:30
8: error_reporting::main
at src\main.rs:38OĂč
flowchart LR
main --> process --> deep --> below_deep
Ce qui est mieux. đ
Qualifier sa chaĂźne dâerreurs
Il serait Ă©galement intĂ©ressant de pouvoir qualifier les erreurs tout au long du parcours dâexĂ©cution.
Pour cela nous allons rajouter une notion de chaĂźne dâerreurs.
Celle-ci nous permettra au besoin de pouvoir parcourir les diffĂ©rentes erreurs ayant conduits Ă lâarrĂȘt de lâexĂ©cution du programme.
enum MyError {
Error {
backtrace: Backtrace,
details: String,
source: Box<Option<MyError>>,
},
Failure,
}Notre chaĂźne dâerreurs sera contenu dans le champ source.
Expliquons son type.
Option<MyError>: signifie la possibilitĂ© dâavoir un maillon infĂ©rieur dans notre chaĂźne dâerreursBox<Option<MyError>>: permet de rendre le champ reprĂ©sentable en mĂ©moire, sans celasourceaurait une taille infinie.
Nous rajoutons alors Ă notre macro raise, une autre possibilitĂ©e dâĂȘtre invoquĂ©e.
macro_rules! raise {
($details:expr) => {
Err(MyError::Error {
backtrace: Backtrace::new(),
details: $details.to_string(),
source: Box::new(None),
})
};
($details:expr, $source:expr) => {
Err(MyError::Error {
backtrace: Backtrace::new(),
details: $details.to_string(),
source: Box::new(Some($source)),
})
};
}Celle-ci prendra en deuxiĂšme paramĂštre une instance de MyError.
Que lâon peut utiliser ainsi:
fn below_deep() -> Result<(), MyError> {
raise!("occurs into below deep function")
}
fn middle_deep() -> Result<(), MyError> {
let result = below_deep();
if let Err(error) = result {
return raise!("was in middle", error);
}
Ok(())
}
fn deep() -> Result<(), MyError> {
middle_deep()?;
raise!("occurs into deep function")
}Dans la mĂ©thode middle_deep, au lieu de propager lâerreur directement, nous allons lâaggrĂ©menter dâinformations supplĂ©mentaires.
Avant de retourner une nouvelle erreur possĂ©dant comme source, lâerreur dĂ©clenchĂ©e par la mĂ©thode below_deep.
let result = below_deep();
if let Err(error) = result {
return raise!("was in middle", error);
}Nous allons également devoir revoir la maniÚre dont nous affichons nos erreurs.
impl MyError {
fn format(&self) -> (Vec<String>, Backtrace) {
let mut outer_details = vec![];
let outer_backtrace = match self {
MyError::Error {
details,
backtrace,
source,
} => {
let mut backtrace = backtrace.clone();
outer_details.push(details.to_string());
if let Some(source) = source.deref() {
let (deep_details, result_backtrace) = source.format();
backtrace = result_backtrace;
outer_details.extend(deep_details.into_iter());
}
backtrace
}
MyError::Failure => unreachable!(),
};
(outer_details, outer_backtrace)
}
}Lâalgorithme est simple: on accumule les dĂ©tails en traversant les diffĂ©rents maillons de la chaĂźne dâerreurs et lâon rĂ©cupĂšre la stacktrace la plus longue et donc celle du maillon le plus profond.
Ce qui nous permet ensuite dâimplĂ©menter le trait Debug
impl Debug for MyError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let (raw_details, backtrace) = self.format();
let details = raw_details.join("\n\tError: ");
write!(f, "Error: {details} \t\n {backtrace:?}")
}
}Puis de lâafficher dans notre main
fn main() {
let val = match process(old_val) {
Ok(val) => {
println!("SuccĂšs");
println!("val : {val}");
}
Err(error @ MyError::Error { .. }) => {
println!("{error:?}");
}
Err(MyError::Failure) => panic!("Crash"),
};
}Cela nous donne:
Error: was in middle
Error: occurs into below deep function
5: error_reporting::below_deep
at src\main.rs:66
6: error_reporting::middle_deep
at src\main.rs:70
7: error_reporting::deep
at src\main.rs:78
8: error_reporting::process
at src\main.rs:83
9: error_reporting::main
at src\main.rs:91Vu que cette partie est un peu longue, je vous donne le code complet:Code Rust
use backtrace::Backtrace;
use std::fmt::{Debug, Formatter};
use std::ops::Deref;
enum MyError {
Error {
backtrace: Backtrace,
details: String,
source: Box<Option<MyError>>,
},
Failure,
}
impl MyError {
fn format(&self) -> (Vec<String>, Backtrace) {
let mut outer_details = vec![];
let outer_backtrace = match self {
MyError::Error {
details,
backtrace,
source,
} => {
let mut backtrace = backtrace.clone();
outer_details.push(details.to_string());
if let Some(source) = source.deref() {
let (deep_details, result_backtrace) = source.format();
backtrace = result_backtrace;
outer_details.extend(deep_details.into_iter());
}
backtrace
}
MyError::Failure => unreachable!(),
};
(outer_details, outer_backtrace)
}
}
impl Debug for MyError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let (raw_details, backtrace) = self.format();
let details = raw_details.join("\n\tError: ");
write!(f, "Error: {details} \t\n {backtrace:?}")
}
}
macro_rules! raise {
($details:expr) => {
Err(MyError::Error {
backtrace: Backtrace::new(),
details: $details.to_string(),
source: Box::new(None),
})
};
($details:expr, $source:expr) => {
Err(MyError::Error {
backtrace: Backtrace::new(),
details: $details.to_string(),
source: Box::new(Some($source)),
})
};
}
fn below_deep() -> Result<(), MyError> {
raise!("occurs into below deep function")
}
fn middle_deep() -> Result<(), MyError> {
let result = below_deep();
if let Err(error) = result {
return raise!("was in middle", error);
}
Ok(())
}
fn deep() -> Result<(), MyError> {
middle_deep()?;
raise!("occurs into deep function")
}
fn process(input: u8) -> Result<u8, MyError> {
deep()?;
println!("After deep");
Ok(input + 1)
}
fn main() {
let val = match process(old_val) {
Ok(val) => {
println!("SuccĂšs");
println!("val : {val}");
}
Err(error @ MyError::Error { .. }) => {
println!("{error:?}");
old_val
}
Err(MyError::Failure) => panic!("Crash"),
};
}
Ă partir de ce moment on peut commencer Ă nous amuser avec le systĂšme.
Par exemple effectuer le code
fn open_registry(registry_name: String) -> Result<(), MyError> {
raise!(format!("Unable to open registry \"{registry_name}\""))
}
fn auth(id: u8) -> Result<(), MyError> {
let result = open_registry(format!("registry_{id}"));
if let Err(error) = result {
return raise!(format!("Unable to open registry for user {id}"), error);
}
Ok(())
}
fn run() -> Result<(), MyError> {
auth(42)?;
raise!("occurs into deep function")
}
fn process(input: u8) -> Result<u8, MyError> {
run()?;
Ok(input + 1)
}Ce qui nous donne comme chaĂźne dâerreur ceci:
Error: Unable to open registry for user 42
Error: Unable to open registry "registry_42"Ces contextes nous permettent de mieux comprendre les erreurs, et ainsi les debugger plus facilement.
Eyre est lĂ pour nous faciliter la vie
Eyre est une crate qui est conçu pour réaliser la totalité des actions que nous avons implémentées plus haut.
La crate présente plusieurs éléments qui permettent de simplifier la vie au développeur.
Lâobjet Result de eyre
Le premier élément de la lib eyre est un objet eyre::Result<T, eyre::Report>.
Pour bĂ©nĂ©ficier de lâerror reporting de eyre, vous devez remplacer vos Result par des eyre::Result.
Par exemple, si vous aviez une fonction qui retournait une std::result::Result<(), CustomError>
fn my_function() -> std::result::Result<(), CustomError> {...}Vous allez devoir la remplacer par
fn my_function() -> eyre::Result<()> {...}Le Report de eyre
Le eyre::Report est lâalternative dâerreur de eyre::Result. Son rĂŽle est dâaccumuler la chaĂźne dâerreur et de fournir le backtrace de la root_cause de lâerreur.
Générer un Report
Afin de faciliter la crĂ©ation dâun Report, la crate eyre fournit une macro eyre! permettant entre autre de gĂ©nĂ©rer un Report qui constituera le premier maillon de notre chaĂźne dâerreurs.
Cette macro est assez souple, elle peut prendre en entré:
- soit une chaine de caractĂšres
- soit une structure ou une énumération qui implémente les traits
DebugetDisplay
Pour une chaine de caractĂšres cela donnerait:
fn my_function() -> eyre::Result<()> {
Err(eyre!("Cette fonction va échouer"))
}De mĂȘme avec la structure ou lâĂ©numĂ©ration implĂ©mentant Debug et Display.
#[derive(Debug)]
enum CustomError {
One,
}
impl Display for CustomError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
fn my_function() -> Result<()> {
Err(eyre!(CustomError::One))
}Wrapper une Erreur dans un Report
Une autre composante trĂšs importante de eyre est sa capacitĂ© Ă venir se greffer Ă nâimporte quelle erreur existante (il peut y avoir des exceptions cependant).
Et venir qualifier des erreurs.
Ceci est rĂ©alisĂ© au moyen dâune mĂ©thode wrap_err.
Par exemple:
fn my_function(path: &str) -> Result<()> {
let file = File::open(path).wrap_err("Unable to open file {path}")?;
Ok(())
}On remarque le Report peut ĂȘtre manipulĂ© comme nâimporte quel Result et donc Ă la capactitĂ© dâĂȘtre propagĂ© au besoin.
Si lâon tente dâafficher le Report
fn my_function(path: &str) -> Result<()> {
let report = File::open(path)
.wrap_err(format!("Unable to open file {path}"))
.unwrap_err();
println!("{report:?}");
Ok(())
}voici ce que lâon obtient:
Unable to open file /tmp
Caused by:
Le fichier spécifié est introuvable. (os error 2)
Location:
src\main.rs:18:10
Stack backtrace:
...Nous avons bien lâerreur primordiale venant de lâOS, et au-dessus le contexte que lâon a utilisĂ© pour wrapper lâerreur systĂšme.
Exemple complet
Si lâon prend un exemple complet cela nous donne quelque chose comme ceci.Code Rust
use eyre::{eyre, Result, WrapErr};
use std::fs::File;
fn open_ledger_internal(path: &str) -> Result<()> {
File::open(path).wrap_err(format!("Unable to open file {path}"))?;
Ok(())
}
fn open_ledger(ledger_id: u32) -> Result<()> {
open_ledger_internal(&format!("/tmp/registry_{ledger_id}"))
.wrap_err(eyre!("Unable to open ledger #{ledger_id}"))?;
Ok(())
}
fn open_registry(registry_name: String) -> Result<()> {
open_ledger(42666).wrap_err(eyre!("Unable to open registry : {registry_name}"))
}
fn auth(id: u8) -> Result<()> {
open_registry(format!("registry_{id}"))
.wrap_err(eyre!("Unable to open registry for user {id}"))?;
Ok(())
}
fn run(id: u8) -> Result<()> {
auth(id).wrap_err(format!("Trying to authenticate user #{id}"))?;
Ok(())
}
fn process(user_id: u8) -> Result<()> {
run(user_id).wrap_err(format!("Running with user #{user_id}"))?;
Ok(())
}
fn main() {
if let Err(report) = process(42) {
println!("{report:?}")
}
}
Qui se matĂ©rialise dans la console, lorsque lâon affiche le Report final en ceci:
Running with user #42
Caused by:
0: Trying to authenticate user #42
1: Unable to open registry for user 42
2: Unable to open registry : registry_42
3: Unable to open ledger #42666
4: Unable to open file /tmp/registry_42666
5: Le chemin dâaccĂšs spĂ©cifiĂ© est introuvable. (os error 3)
Location:
src\main.rs:5:22
Stack backtrace:
10: error_reporting::open_ledger_internal
at .\src\main.rs:5
11: error_reporting::open_ledger
at .\src\main.rs:11
12: error_reporting::open_registry
at .\src\main.rs:17
13: error_reporting::auth
at .\src\main.rs:21
14: error_reporting::run
at .\src\main.rs:28
15: error_reporting::process
at .\src\main.rs:33
16: error_reporting::main
at .\src\main.rs:38Cette stacktrace ainsi que les dĂ©tails nous permettent dâavoir une vue complĂšte de lâexĂ©cution de notre programme.
Information
Bien sĂ»r, ici nous avons wrappĂ© dâun contexte tous les
Resultdes diffĂ©rents retours, mais nous aurions trĂšs bien pu juste propager lâerreur.Si lâon modifie dans le code ci-dessus la mĂ©thode
authpour retirer le contexte.fn auth(id: u8) -> Result<()> { open_registry(format!("registry_{id}")?; Ok(()) }La chaĂźne de contextes dâerreurs devient:
Running with user #42 Caused by: 0: Trying to authenticate user #42 1: Unable to open registry : registry_42 2: Unable to open ledger #42666 3: Unable to open file /tmp/registry_42666 4: Le chemin dâaccĂšs spĂ©cifiĂ© est introuvable. (os error 3)Ainsi nul besoin, de vouloir absolument wrapper tous ses
Report. La crate est suffisamment souple pour nous permettre de rajouter du contexte uniquement lĂ oĂč cela est nĂ©cessaire.
Gestion de lâaffichage des Reports
un Report possĂšde plusieurs façon de sâafficher:
Debug
Sâaffiche avec
println!("{report:?}")Donne la derniĂšre erreur connue, les details de chaque erreur de la chaĂźne ainsi que la stacktrace conduisant Ă lâerreur.
Running with user #42
Caused by:
0: Trying to authenticate user #42
1: Unable to open registry for user 42
2: Unable to open registry : registry_42
3: Unable to open ledger #42666
4: Unable to open file /tmp/registry_42666
5: Le chemin dâaccĂšs spĂ©cifiĂ© est introuvable. (os error 3)
Location:
src\main.rs:5:22
Stack backtrace:
...Display
Sâaffiche avec:
println!("{report}")Donne seulement la derniĂšre erreur connu et cache la raison primordiale.
Running with user #42Display alternatif
Sâaffiche avec:
println!("{report:#}")Donne toutes les raisons de la chaĂźne dâerreurs sur une seule ligne, cache la stacktrace.
Running with user #42: Trying to authenticate user #42: Unable to open registry : registry_42: Unable to open ledger #42666: Unable to open file /tmp/registry_42666: Le
chemin dâaccĂšs spĂ©cifiĂ© est introuvable. (os error 3)Utiliser eyre dans un exemple concret
Dans un prĂ©cĂ©dent article nous avions uilisĂ© un certain nombre de lib pour Ă la fois effectuer des requĂȘtes sur le web et dĂ©sĂ©rialiser des donnĂ©es.
Ces bibliothÚques possÚdent des erreurs spécifiques qui sont incompatibles entre-elles.
Ce qui rend compliquĂ©e la gestion immĂ©diate des erreurs qui doivent ĂȘtre mappĂ© vers une Erreur commune.
Ă lâĂ©poque nous avions utilisĂ© la crate thiserror pour rĂ©aliser cette conversion.
use thiserror::Error;
#[derive(Error, Debug)]
enum CustomError {
#[error("sqlite errors {0:?}")]
Sqlite(sqlite::Error),
#[error("reqwest errors")]
Reqwest(reqwest::Error),
#[error("json errors")]
Json(serde_json::Error),
}
impl From<sqlite::Error> for CustomError {
fn from(err: sqlite::Error) -> Self {
CustomError::Sqlite(err)
}
}
impl From<serde_json::Error> for CustomError {
fn from(err: serde_json::Error) -> Self {
CustomError::Json(err)
}
}
impl From<reqwest::Error> for CustomError {
fn from(err: reqwest::Error) -> Self {
CustomError::Reqwest(err)
}
}Nous allons réécrire la mĂȘme gestion dâerreurs mais en gĂ©rant cette fois-ci nos erreur dans un eyre::Report.
Pour plus de clartĂ© je ne nâimplĂ©menterai pas la partie sqlite
La fonction qui nous intéresse est celle ci:
#[tracing::instrument]
async fn fetch_user(id: u32) -> Result<User> {
let url = format!("https://jsonplaceholder.typicode.com/users/{}", id);
let response = reqwest::get(url).await?;
let response_string = response.text().await?;
let user = serde_json::from_str(response_string.as_str())?;
Ok(user)
}Elle possĂšde Ă la fois des erreurs de type reqwest et serde_json, grĂące Ă la bibliothĂšque thiserror et les impl From<Error> cela est plus ou moins transparent.
Mais, cela reprĂ©sente tout de mĂȘme un certain volume de code de boilerplate.
Voyons comment réécrire cela grùce à eyre.
use eyre::{eyre, Result, WrapErr};
use serde::Deserialize;
#[derive(Deserialize)]
struct User {
id: u32,
name: String,
email: String,
phone: String,
website: String,
}
async fn fetch_user(url: &str) -> Result<User> {
let response = reqwest::get(url).await?;
let response_string = response.text().await?;
let user = serde_json::from_str(response_string.as_str())?;
Ok(user)
}
#[tokio::main]
async fn main() {
let url = "https://jsonplaceholder.typicode.com/users/1";
if let Err(report) = fetch_user(url).await {
println!("{report:?}")
}
}Je vous fournis également le Cargo.toml
[dependencies]
eyre = "0.6.8"
reqwest = { version = "0.11.11", features = ["json"] }
serde = { version = "1.0.140", features = ["derive"] }
serde_json = "1.0.82"
thiserror = "1.0.31"
tokio = {version = "1.20.1", features = ["rt-multi-thread", "macros"]}Câest plus simple non ? đ€©
On essaie la gestion dâerreur
Pas dâerreur
Pour une url=https://jsonplaceholder.typicode.com/users/1: pas de soucis.
Reqwest
Pour une url=https://jsonplaceholder.typicode.com2/users/1, le report renvoit:
error sending request for url (https://jsonplaceholder.typicode.com2/users/1): error trying to connect: dns error: HĂŽte inconnu. (os error 11001)
Caused by:
0: error trying to connect: dns error: HĂŽte inconnu. (os error 11001)
1: dns error: HĂŽte inconnu. (os error 11001)
2: HĂŽte inconnu. (os error 11001)
Location:
src\main.rs:14:20
Stack backtrace:
10: error_reporting::fetch_user::async_fn$0
at .\src\main.rs:14
12: error_reporting::main::async_block$0
at .\src\main.rs:23
24: error_reporting::main
at .\src\main.rs:23Le sytĂšme tente de rĂ©aliser un call HTTP sur le domaine âjsonplaceholder.typicode.com2â, mais celui-ci nâexiste pas.
Json
Pour une url=https://jsonplaceholder.typicode.com/users/22, le report renvoit:
missing field `id` at line 1 column 2
Location:
src\main.rs:15:16
Stack backtrace:
10: error_reporting::fetch_user::async_fn$0
at .\src\main.rs:15
12: error_reporting::main::async_block$0
at .\src\main.rs:23
24: error_reporting::main
at .\src\main.rs:23Au-delĂ de 10, lâAPI renvoit un json vide. La phase de dĂ©sĂ©rialisation Ă©choue car elle nâarrive pas Ă trouver le champ id.
Enrichir les erreurs
Cette erreur de dĂ©sĂ©rialisation nâest pas claire, essayons de rajouter un peu plus de contexte.
async fn fetch_user(url: &str) -> Result<User> {
let response = reqwest::get(url).await?;
let response_string = response.text().await?;
let user = serde_json::from_str(response_string.as_str())
.wrap_err(format!("Error while trying to deserialize user from {url}"))?;
Ok(user)
}Error while trying to deserialize user from https://jsonplaceholder.typicode.com/users/22
Caused by:
missing field `id` at line 1 column 2Et bien, voilĂ ! Câest beaucoup mieux! đ
Le debugging sera bien plus simple ainsi.
Et nous nous sommes débarassé de beaucoup de code qui venait parasiter notre compréhension des choses.
Nous nous sommes également épargné un crate supplémentaire.
Utiliser eyre dans le contexte dâune lib
La documentation officielle prĂ©vient quâil nâest pas recommendĂ© dâexposer directement un eyre::Result comme sortie dâerreur publique de votre lib.
La crĂ©atrice de eyre en a fait une rĂ©ponse dans un thread dâissue github.
Pour simuler cette exposition dâerreurs par une lib externe, je vous propose dâutiliser ce projet que nous allons dĂ©tailler ensemble.
Le projet est composĂ© dâun workspace contenant un sous projet user, ce sous-projet sera notre lib externe exposant une API dâerreur spĂ©cifique.
Le main.rs principal est plutĂŽt simple, il dĂ©clare le rĂ©acteur asynchrone de tokio et affiche soit lâutilisateur si trouvĂ©, soit lâerreur venant de la lib user si ce nâest pas le cas.
#[tokio::main]
async fn main() {
let url = "https://jsonplaceholder.typicode.com/users/2";
match user::run(url).await {
Ok(user) => {
println!("{user:?}");
}
Err(error) => {
eprintln!("{error}")
}
}
}Dans la lib user, la méthode fetch_user ne bouge pas.
Enfin on introduit dans lib user une méthode run dont son rÎle est de transformer le Report de fetch_user en une UserError.
use thiserror::Error;
#[derive(Debug, Error)]
pub enum UserError {
#[error("Unable to fetch url: {0}")]
Fetch(String),
#[error("Unable to unmarshal user data: {0}")]
Unmarshal(String),
#[error("Unknown error")]
Unknown,
}On réutilise ici la crate thiserror pour nous simplifier la création des erreurs, cette crate nous permettant dimplémenté le trait Display.
On va ici introduire une fonctionnalité essentielle de eyre: le déréférencement !
LâidĂ©e est de rĂ©cupĂ©rer lâerreur qui a Ă©tĂ© wrap par le Report.
Imaginons la situation suivante, si lâon wrap une erreur venant dâun File::open, nous allons obtenir un Report contenant une io::Error.
Pour récupérer cette io::Error, nous allons devoir la déréférencer du Report.
let result : Result<File, Report> = File::open("\tmp\not\exists").wrap_err("This file not exists");
if let Err(report) = result {
if let Some(io_error: &io::Error) = report.downcast_ref::<io::Error>() {
// ...
}
}Ici le plus important Ă comprendre câest lâappel Ă report.downcast_ref::<E>(), E Ă©tant le type vers lequel lâon souhaite caster le Report.
Ce qui fait quâĂ partir de ce moment lĂ on est capable de savoir le type exact de lâerreur qui vient dâĂȘtre levĂ©e.
La deuxiÚme capacité de downcast_ref, est de pouvoir également extraire le contexte du Report.
if let Err(report) = result {
if let Some(io_error: &io::Error) = report.downcast_ref::<io::Error>() {
if let Some(details: &&str) = report.downcast_ref::<&str>() {
// details == "This file not exists"
}
}
}Attention
Si le contexte provient dâun
format!alors son type estStringet non&str.Il faut donc modifier le downcast en conséquence!
let result : Result<File, Report> = File::open("\tmp\not\exists") .wrap_err(format!("File {} not exists", "\tmp\not\exists")); if let Err(report) = result { if let Some(io_error: &io::Error) = report.downcast_ref::<io::Error>() { if let Some(details: &String) = report.downcast_ref::<String>() { // details == "This file not exists" } } }
Revenons maintenant Ă notre lib user.
Notre fonction run, prend en paramĂštre une URL et effectue un fetch_user.
Sâil y a erreur, elle transforme le Report en une UserError qui peut ensuite ĂȘtre prĂ©sentĂ©e publiquement.
pub async fn run(url: &str) -> Result<User, UserError> {
match fetch_user(url).await {
Ok(user) => Ok(user),
Err(report) => {
// Gestion d'une erreur de fetch
if let Some(reqwest_error) = report.downcast_ref::<reqwest::Error>() {
return Err(UserError::Fetch(reqwest_error.to_string()));
}
// Gestion d'une erreur de désérialisation
if let Some(serde_error) = report.downcast_ref::<serde_json::Error>() {
// Si du contexte est accroché à l'erreur on le récupÚre
if let Some(details) = report.downcast_ref::<String>() {
return Err(UserError::Unmarshal(details.to_string()));
}
// Sinon on renvoie l'erreur brute
return Err(UserError::Unmarshal(serde_error.to_string()));
}
// On gĂšre aussi le cas d'une erreur inconnue
Err(UserError::Unknown)
}
}
}Faisons quelques essais.
url=âhttps://jsonplaceholder.typicode.com/users/2â
User { id: 2, name: âErvin Howellâ, email: âShanna@melissa.tvâ, phone: â010-692-6593 x09125â, website: âanastasia.netâ }url=âhttps://jsonplaceholder.typicode.com/users/22â
Unable to unmarshal user data: Error while trying to deserialize user from https://jsonplaceholder.typicode.com/users/22url=âhttps://jsonplaceholder.typicode.com2/users/2â
Unable to fetch url: error sending request for url (https://jsonplaceholder.typicode.com2/users/): error trying to connect: dns error: HĂŽte inconnu. (os error 11001)Conclusion
Et VoilĂ !
Comme toujours, il y aurait encore tant de chose Ă dire, mais nous allons nous arrĂȘter lĂ pour aujourdâhui.
JâespĂšre que la dĂ©couverte de la bibliothĂšque eyre vous a plu, et que la restropective de la gestion dâerreur aussi! đ
Je vous souhaite une bonne journĂ©e et merci de mâavoir lu. â€ïž