Introduction : Pourquoi le try/catch standard ne suffit-il pas ?
\n\nLe développement en Kotlin, en particulier dans le contexte des applications serveur ou Android, est souvent confronté à la nécessité d'une gestion fiable des erreurs. Le bloc try/catch standard est la base, mais en pratique, il conduit à une « dispersion » de la logique et à une complexification des tests. À un niveau avancé, nous voulons non seulement attraper les exceptions, mais concevoir le système de sorte que les erreurs fassent partie du type de données, et non d'un flux de contrôle inattendu.
Dans cet article, nous verrons comment dépasser le cadre de try/catch et utiliser des approches fonctionnelles : Result, Either (provenant de bibliothèques), et les sealed class. Vous apprendrez à rendre le code prévisible, typé et facile à maintenir.
1. Try/catch classique : quand est-il encore utile ?
\n\nIl ne faut pas abandonner complètement try/catch. Il est indispensable pour travailler avec du code legacy, des bibliothèques externes (par exemple, lors de l'analyse JSON) et les appels système (entrées-sorties). Cependant, il y a des nuances importantes.
Structure correcte et ressources
\nEn Kotlin, contrairement à Java, il n'y a pas d'exceptions vérifiées, mais il existe une construction élégante use pour la fermeture automatique des ressources :
fun readFile(path: String): String? = try {\n BufferedReader(FileReader(path)).use { it.readText() }\n} catch (e: IOException) {\n println("Erreur de lecture du fichier : ${e.message}")\n null\n}\n\nIci, use garantit la fermeture du fichier, même si une exception se produit à l'intérieur. C'est un remplacement élégant de try-with-resources.
Problème : perte de contexte
\nLe principal inconvénient de try/catch est qu'il n'informe pas le code appelant que la fonction peut « échouer ». L'exception est un effet secondaire caché. L'appel à readFile() peut retourner null ou lever une IOException, mais la signature de la fonction ne le reflète pas. Cela conduit à des erreurs à l'exécution.
La solution est de passer à une représentation explicite des erreurs.
\n\n2. Result : une monade intégrée pour le succès ou l'échec
\n\nKotlin fournit une classe standard Result<T>. C'est une structure algébrique simple : soit un succès (valeur T), soit un échec (Throwable). Cela fait de la gestion des erreurs une partie du système de types.
Utilisation de base
\nfun riskyDivision(a: Int, b: Int): Result<Int> = runCatching {\n a / b\n}\n\nfun main() {\n val result = riskyDivision(10, 0)\n result.onSuccess { println("Résultat : $it") }\n .onFailure { println("Erreur : ${it.message}") }\n}\n\nrunCatching est une enveloppe autour de try/catch qui retourne un Result. Les méthodes onSuccess et onFailure permettent de réagir au résultat sans interrompre la chaîne d'appels.
Transformation et chaînage
\nVous pouvez transformer le résultat sans vous soucier des exceptions à l'intérieur de map :
\n\nval finalResult = riskyDivision(10, 2)\n .map { it * 2 } // si succès — on multiplie\n .recover { error -> // si erreur — on retourne une valeur de repli\n println("Utilisation d'une valeur de repli à cause de : ${error.message}")\n 0\n }\nprintln(finalResult.getOrDefault(-1)) // 10\n\nImportant : Result n'est pas conçu pour être stocké dans des collections ou transmis entre threads sans traitement supplémentaire. Pour ces cas, les sealed class ou des bibliothèques comme Arrow sont plus adaptées.
3. Either : distinguer le type d'erreur
\n\nLa classe Result ne capture que les Throwable. Mais souvent, nous voulons distinguer les types d'erreurs : « non trouvé », « droits insuffisants », « erreur de validation ». C'est là que Either<L, R> des bibliothèques fonctionnelles (par exemple, Arrow ou une implémentation personnalisée) vient à la rescousse.