Gestion avancée des erreurs en Kotlin : de try/catch aux approches fonctionnelles
La gestion des erreurs est l'une des tâches clés lors du développement de logiciels fiables. En Kotlin, en plus du mécanisme classique try/catch/finally, il existe tout un arsenal d'outils avancés : Result, runCatching, sealed class et les monades fonctionnelles. Dans cet article, nous allons voir comment dépasser l'utilisation de base et rendre votre code plus sûr, lisible et prévisible.
1. Try/catch classique : quand est-il encore approprié ?
Le try/catch de base en Kotlin est une expression qui peut retourner une valeur. C'est la première chose qui le distingue de Java.
val result: String = try { riskyOperation()} catch (e: IOException) { "Erreur d'entrée/sortie : ${e.message}"} catch (e: Exception) { "Erreur inconnue : ${e.message}"} finally { println("Le bloc finally s'exécute toujours")}Malgré sa simplicité, cette approche présente des inconvénients :
- Les exceptions sont une opération coûteuse (création de la stack trace).
- Le type d'erreur n'est pas fixé au niveau des types — il est facile d'oublier de la traiter.
- Le code devient lourd avec un grand nombre d'opérations imbriquées.
Quand utiliser try/catch ?
- Lors du travail avec des ressources externes (fichiers, réseau, bases de données).
- Dans le code de bibliothèque, où les exceptions font partie du contrat.
- Pour les erreurs critiques qui ne peuvent pas être traitées localement.
2. Approche fonctionnelle : Result et runCatching
Kotlin propose une classe intégrée Result qui représente le résultat d'une opération pouvant réussir ou échouer. C'est une alternative aux exceptions, ne nécessitant pas de stack trace coûteuse.
fun divide(a: Int, b: Int): Result<Int> { return runCatching { a / b }}
fun main() { val result = divide(10, 0) result.onSuccess { value -> println("Résultat : $value") }.onFailure { error -> println("Erreur : ${error.message}") }}Méthodes de Result
getOrNull()— retourne la valeur ou null.getOrDefault(default)— retourne la valeur ou une valeur par défaut.mapetflatMap— pour les chaînes de transformations.recover— récupération après une erreur.
val safeResult = runCatching { riskyOperation() } .map { it.toUpperCase() } .recover { "default_value" } .getOrDefault("fallback")Important : Result n'est pas conçu pour être transmis entre threads ou stocké dans des champs de classe — utilisez-le uniquement comme valeur locale.
3. Classes sealed : modélisation des erreurs au niveau des types
Lorsque vous devez définir clairement tous les états possibles (succès, différents types d'erreurs), les sealed class viennent à la rescousse. C'est un outil puissant pour créer des types de données algébriques.
sealed class NetworkResult<out T> { data class Success<T>(val data: T) : NetworkResult<T>() data class Error(val message: String, val code: Int) : NetworkResult<Nothing>() object Loading : NetworkResult<Nothing>()}
fun fetchUser(id: Int): NetworkResult<User> { return try { val user = api.getUser(id) NetworkResult.Success(user) } catch (e: HttpException) { NetworkResult.Error("Le serveur a retourné une erreur", e.code()) } catch (e: Exception) { NetworkResult.Error("Erreur inconnue", 0) }}
fun handleResult(result: NetworkResult<User>) { when (result) { is NetworkResult.Success -> println("Utilisateur : ${result.data.name}") is NetworkResult.Er