runCatching en Kotlin : gérer les erreurs proprement avec Result
En Kotlin, la gestion des erreurs se fait souvent avec try/catch.
Mais pour certains cas (pipelines, transformations, retours de fonctions), on préfère manipuler
une valeur “succès ou échec” de manière fonctionnelle.
C’est exactement ce que permet runCatching : exécuter un bloc et obtenir un
Result<T> qui contient soit une valeur, soit une exception.
1) Qu’est-ce que runCatching ?
runCatching exécute un bloc et capture toute exception levée (Throwable) dans un Result.
val result: Result<Int> = runCatching {
"123".toInt()
}
Si le bloc réussit ? Result.success(value).
S’il échoue ? Result.failure(exception).
2) Comparaison avec try/catch
2.1 try/catch classique
val value: Int = try {
"123".toInt()
} catch (e: NumberFormatException) {
0
}
2.2 La même chose avec runCatching + getOrElse
val value = runCatching { "123".toInt() }
.getOrElse { 0 }
L’avantage : tu peux enchaîner proprement des transformations avant de décider comment gérer l’erreur.
3) Lire un Result : getOrNull, exceptionOrNull, getOrThrow
Quelques méthodes utiles :
getOrNull(): retourne la valeur ounull.exceptionOrNull(): retourne l’exception ounull.getOrThrow(): relance l’exception si échec.
val result = runCatching { "abc".toInt() }
val valueOrNull = result.getOrNull() // null
val errorOrNull = result.exceptionOrNull() // NumberFormatException
4) Traiter succès / échec : onSuccess / onFailure
Pour déclencher des effets (log, analytics, UI), on utilise souvent :
runCatching { loadUserFromNetwork() }
.onSuccess { user ->
println("Utilisateur chargé : $user")
}
.onFailure { error ->
println("Erreur : ${error.message}")
}
Ces fonctions ne transforment pas le résultat : elles sont faites pour des “side effects”.
5) Transformer le succès : map / mapCatching
Si tu veux transformer la valeur en cas de succès :
val result = runCatching { "123" }
.map { it.toInt() } // map ne catch pas les exceptions du bloc map
Attention : map ne capture pas les exceptions levées pendant la transformation
(selon versions, comportement subtil).
Pour être sûr de capturer une exception dans la transformation, utilise mapCatching.
val result = runCatching { "abc" }
.mapCatching { it.toInt() } // capture l'exception dans le Result
6) Transformer l’échec : recover / recoverCatching
Si tu veux “récupérer” une erreur en fournissant une valeur de remplacement :
val value = runCatching { "abc".toInt() }
.recover { 0 }
.getOrThrow()
Comme pour map, si ton code de récupération peut lui-même échouer, utilise recoverCatching.
val result = runCatching { fetchToken() }
.recoverCatching { error ->
// peut aussi lancer une exception
refreshTokenOrThrow()
}
7) Chaîner plusieurs opérations
runCatching est pratique quand tu veux enchaîner des étapes
en gardant un seul “pipeline” d’erreur.
val result = runCatching { readFile("config.json") }
.mapCatching { parseConfig(it) }
.mapCatching { validateConfig(it) }
Tu peux ensuite décider :
val config = result.getOrElse { defaultConfig() }
8) Usage typique en architecture (Domain / Data)
Une pratique courante est d’exposer un résultat explicite depuis le repository :
fun getUser(id: String): Result<User> =
runCatching { api.getUser(id) }
Puis, côté appelant :
val user = repository.getUser("42")
.getOrElse { return showError(it) }
Cela évite de laisser des exceptions “sortir” sans contrôle.
9) Pièges importants à connaître
9.1 Capturer “trop large”
runCatching capture des Throwable.
Si tu utilises cela partout, tu risques de masquer des erreurs critiques
(ex : OOM, erreurs de programmation) au lieu de les corriger.
En pratique, on réserve runCatching à des blocs où l’échec est “normal” :
- parsing,
- IO,
- réseau,
- conversion,
- accès à des données externes.
9.2 Coroutines : ne pas avaler CancellationException
En coroutines, l’annulation est souvent portée par une CancellationException.
Si tu la captures et la transformes en Result.failure, tu peux casser
des comportements d’annulation (tâches qui continuent alors qu’elles devraient s’arrêter).
Bon réflexe : relancer explicitement l’annulation :
import kotlinx.coroutines.CancellationException
suspend fun safeCall(): Result<String> =
runCatching {
callNetwork()
}.onFailure { e ->
if (e is CancellationException) throw e
}
9.3 Trop de Result partout peut rendre le code lourd
Il ne faut pas remplacer tous les try/catch par runCatching.
Souvent :
- dans du code simple ?
try/catchest plus direct, - dans des pipelines ?
runCatchingest plus lisible.
10) Quand utiliser runCatching ?
- Quand tu veux transformer des erreurs en “valeur manipulable”.
- Quand tu veux chaîner des étapes avec
mapCatching/recover. - Quand tu veux un retour “succès/échec” explicite dans une API.
Si tu veux juste gérer une exception localement, try/catch reste souvent le plus clair.
Conclusion
runCatching est un outil très pratique pour encapsuler des exceptions dans un Result
et construire un flux de traitement lisible :
runCatchingpour exécuter un bloc,mapCatchingpour transformer le succès en capturant les erreurs,recover/recoverCatchingpour gérer les échecs,onSuccess/onFailurepour logguer ou déclencher des effets.
Bien utilisé, il rend le code plus robuste et plus expressif. Mal utilisé (capture trop large, annulation avalée), il peut masquer des bugs.