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 ou null.
  • exceptionOrNull() : retourne l’exception ou null.
  • 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/catch est plus direct,
  • dans des pipelines ? runCatching est 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 :

  • runCatching pour exécuter un bloc,
  • mapCatching pour transformer le succès en capturant les erreurs,
  • recover / recoverCatching pour gérer les échecs,
  • onSuccess / onFailure pour 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.