La gestion des erreurs est un aspect crucial du développement logiciel. Une gestion inadéquate peut entraîner des bugs silencieux, des pertes financières et une expérience utilisateur médiocre. Cet article explore les approches de gestion des erreurs en JavaScript et en Rust, en mettant l'accent sur les limites de JavaScript et les avantages de Rust avec le framework Hyperlane.
L'évolution de la gestion des erreurs en JavaScript
Le paysage de la gestion des erreurs en JavaScript a évolué au fil du temps, passant des callbacks aux Promises, puis à async/await.
1. L'ère des Callbacks
Initialement, Node.js utilisait un modèle "error-first callbacks", où la première arguement d'une fonction de callback était dédié à l'erreur.
function traiterCommande(idCommande, callback) {
db.trouverCommande(idCommande, (err, commande) => {
if (err) {
// Point de gestion d'erreur 1
return callback(err);
}
paiement.traiter(commande, (err, resultat) => {
if (err) {
// Point de gestion d'erreur 2
return callback(err);
}
inventaire.mettreAJour(commande.articles, (err, statut) => {
if (err) {
// Point de gestion d'erreur 3
return callback(err);
}
callback(null, statut); // Succès
});
});
});
}
Bien que conceptuellement simple, ce modèle conduisait rapidement à une structure pyramidale difficile à maintenir, où l'oubli d'une vérification d'erreur entraînait son "absorption" silencieuse.
2. L'avènement des Promises
Les Promises ont introduit une manière plus plate de gérer l'asynchronisme, avec les méthodes .then() et .catch().
function traiterCommande(idCommande) {
return db
.trouverCommande(idCommande)
.then((commande) => paiement.traiter(commande))
.then((resultat) => inventaire.mettreAJour(resultat.articles))
.catch((err) => {
// Point de gestion d'erreur centralisé
console.error('Échec du traitement de la commande:', err);
// Il faut explicitement relancer l'erreur pour que le chaîneur supérieur soit notifié
throw err;
});
}
Cependant, le risque d'erreurs silencieuses persistait si une Promise n'était pas correctement retournée ou si une erreur n'était pas relancée dans un bloc .catch().
3. La clarté d'async/await
async/await offre une syntaxe proche du code synchrone, améliorant la lisibilité.
async function traiterCommande(idCommande) {
try {
const commande = await db.trouverCommande(idCommande);
const resultat = await paiement.traiter(commande);
const statut = await inventaire.mettreAJour(resultat.articles);
return statut;
} catch (err) {
console.error('Échec du traitement de la commande:', err);
throw err; // Nécessité de relancer l'erreur
}
}
Malgré cette amélioration, la gestion des erreurs reste dépendante de la discipline du développeur. L'oubli d'un await sur une fonction retournant une Promise peut laisser les erreurs non détectées par le bloc try...catch.
En JavaScript, les erreurs peuvent être facilement ignorées. Les valeurs null et undefined peuvent se propager sans être remarquées, nécessitant une discipline stricte et des outils de linting pour garantir leur traitement.
Le paradigme Result en Rust et Hyperlane
Rust adopte une approche radicalement différente avec son énumération Result<t e="">.
enum Result<t e=""> {
Ok(T), // Succès, contenant une valeur de type T
Err(E), // Échec, contenant une erreur de type E
}
</t>
Cette énumération garantit qu'une fonction potentiellement défaillante retourne toujours soit un succès, soit un échec. Le compilateur Rust impose le traitement de la branche Err, rendant impossible l'ignorance d'une erreur.
Dans le framework Hyperlane, cela se traduit par un code plus sûr et concis :
// Dans un fichier de service
pub fn traiter_commande(id_commande: &str) -> Result<status ordererror=""> {
// L'opérateur `?` propage automatiquement l'erreur en cas d'échec
let commande = db::trouver_commande(id_commande)?;
let resultat = paiement::traiter(commande)?;
let statut = inventaire::mettre_a_jour(resultat.articles)?;
Ok(statut) // Retour explicite en cas de succès
}
</status>
L'opérateur ? simplifie considérablement la propagation des erreurs, transformant la gestion des échecs en une partie intégrante et naturelle du flux de données.
panic\_hook : une dernière ligne de défense
Pour les erreurs imprévisibles (paniques), comme les dépassements d'entiers ou les accès hors limites, Hyperlane offre un panic\_hook.
// Dans le fichier de configuration du serveur
async fn gestionnaire_panique(ctx: Context) {
let erreur_panique: Panique = ctx.try_get_panic().await.unwrap_or_default();
let corps_reponse: String = erreur_panique.to_string();
eprintln!("{}", corps_reponse); // Journalisation détaillée
// Réponse standardisée au client
let _ = ctx
.set_response_status_code(500)
.await
.set_response_body("Erreur interne du serveur")
.await
.send()
.await;
}
// Enregistrement du hook dans la fonction principale
serveur.panic_hook(gestionnaire_panique).await;
Ce mécanisme empêche le crash du serveur, permet la journalisation des erreurs et renvoie une réponse d'erreur générique au client, assurant ainsi la robustesse.
En conclusion, Rust et Hyperlane transforment la gestion des erreurs d'une contrainte de discipline du développeur en une garantie fournie par le compilateur et le framework. Cette approche favorise la création de logiciels plus fiables et résilients.