Error Propagation est un mécanisme qui propage une erreur vers le haut de la pile d'appels du point d'origine jusqu'au gestionnaire. Lorsqu'une fonction ne peut pas traiter une erreur elle-même, elle la transmet à l'appelant via une exception, une déclaration throws ou un Return Type. Une implémentation correcte de la propagation est essentielle pour la stabilité des applications mobiles : les erreurs non traitées ou mal transmises entraînent des crashes. Selon Apple Swift Documentation (2026), la propagation automatique via throws dans Swift permet de transmettre une erreur à n'importe quel niveau sans code passe-partout.
Points clés
Error Propagation (propagation d'erreur) est le processus de transmission d'un objet d'erreur de la fonction où il s'est produit vers le haut de la chaîne d'appels jusqu'au gestionnaire approprié le plus proche. Imaginez la pile d'appels : ViewController appelle ViewModel, ViewModel appelle Repository, Repository appelle API. Si l'API renvoie une erreur réseau, elle doit traverser Repository et ViewModel jusqu'à ViewController, qui affichera un message à l'utilisateur. Chaque fonction intermédiaire décide : traiter l'erreur ou la transmettre plus haut (propager).
Il existe deux approches de la propagation : automatique et manuelle. Avec l'approche automatique (Swift throws, exceptions vérifiées Java), le compilateur oblige le développeur à traiter l'erreur ou à déclarer la propagation dans la signature. Avec l'approche manuelle (Result Type, Kotlin Try), l'erreur est transmise comme une valeur — le développeur écrit explicitement du code pour transmettre ou transformer l'erreur. Selon Kotlin Result Docs (2026), Result<T> dans Kotlin n'est pas conçu pour la propagation directe à travers les limites de fonctions — il doit être transformé ou traité à chaque niveau, ce qui rend la propagation plus consciente mais aussi plus verbeuse.
Le choix de l'approche dépend de l'architecture de l'application et du langage. Dans Swift, la propagation automatique via throws domine ; dans Kotlin, on utilise un mélange d'exceptions (pour les erreurs inattendues) et de conteneurs de type Result (pour les erreurs attendues). Il est important de comprendre : la propagation n'est pas un objectif, mais une nécessité. Une architecture idéale minimise la profondeur de la propagation, en traitant les erreurs au niveau le plus bas possible où il y a suffisamment de contexte pour prendre une décision.
Dans Swift, la propagation via throws est automatique : si la fonction A avec throws appelle la fonction B avec throws, et que A ne traite pas l'erreur de B dans do-catch, l'erreur est automatiquement transmise à l'appelant de A. Cela élimine le code passe-partout typique des exceptions vérifiées Java, où throws doit être déclaré dans chaque méthode de la chaîne. Swift utilise le principe « une fonction throws dans la chaîne = toute la chaîne devient throws, si elle n'est pas traitée aux niveaux intermédiaires ».
struct UserRepository {
func fetchUser(id: Int) throws -> User {
let data = try networkService.request(path: "/users/\(id)")
return try parseUser(from: data)
}
}
class UserViewModel {
let repo = UserRepository()
func loadUser(id: Int) throws -> User {
return try repo.fetchUser(id: id)
}
}
// ViewController — gestionnaire final
func onButtonTap() {
let vm = UserViewModel()
do {
let user = try vm.loadUser(id: 42)
updateUI(user)
} catch {
showError("Échec du chargement de l'utilisateur")
}
}
Chaîne de propagation : networkService.request -> fetchUser -> loadUser -> onButtonTap. Chaque fonction intermédiaire est marquée throws et ne contient pas de do-catch — l'erreur est automatiquement transmise vers le haut. ViewController onButtonTap est le gestionnaire final avec do-catch. Si ViewModel décidait de transformer l'erreur (l'envelopper dans un autre type), il pourrait utiliser do-catch et un nouveau throw. La propagation automatique réduit le code : Repository n'a pas besoin de savoir comment traiter l'erreur — c'est la responsabilité de ViewController, qui a accès à l'interface utilisateur pour afficher un message à l'utilisateur.
Dans Kotlin, la propagation via les exceptions ne nécessite pas de déclaration throws dans la signature (toutes les exceptions sont non vérifiées). L'exception remonte automatiquement la pile jusqu'à ce qu'elle rencontre un try-catch. Cependant, l'absence de throws dans la signature rend la propagation implicite : le développeur ne peut pas voir à partir de la signature de la fonction qu'elle pourrait lever une exception. C'est à la fois un avantage (moins de code passe-partout) et un inconvénient (plus facile d'oublier de traiter). Kotlin résout ce problème par le biais de conventions et de modèles architecturaux, et non par le langage lui-même.
class UserRepository(
private val api: ApiService,
private val db: Database
) {
suspend fun getUser(id: String): User {
return try {
api.fetchUser(id)
} catch (e: IOException) {
db.getCachedUser(id) ?: throw AppException("User unavailable")
}
}
}
class UserViewModel(private val repo: UserRepository) {
private val _state = MutableStateFlow<UiState<User>>(UiState.Loading)
val state: StateFlow<UiState<User>> = _state
fun loadUser(id: String) {
viewModelScope.launch {
try {
val user = repo.getUser(id)
_state.value = UiState.Success(user)
} catch (e: AppException) {
_state.value = UiState.Error(e.message ?: "Unknown")
}
}
}
}
Dans Repository, propagation avec transformation : lors d'une IOException (réseau indisponible), la fonction tente d'obtenir les données mises en cache depuis la base de données. Si le cache est vide, elle lève AppException — la propagation continue avec un nouveau type d'erreur. ViewModel attrape AppException et le traduit en UiState.Error — l'erreur ne va pas plus loin, la propagation se termine au niveau de la couche UI. Kotlin Coroutines ajoute des particularités : les exceptions dans launch se propagent automatiquement via CoroutineExceptionHandler, et dans async — seulement lors de l'appel à await(). Ceci est important lors de la conception de la propagation dans les coroutines — SupervisorJob empêche l'annulation de la coroutine parente en cas d'erreur dans une coroutine enfant.
Une alternative aux exceptions est la propagation via un type conteneur qui transmet le succès ou l'erreur comme une valeur. Dans cette approche, la fonction ne renvoie pas une valeur mais une enveloppe : Result<T, E> dans Swift, Result<T> dans Kotlin, Either<L, R> dans Dart (du paquet fpdart ou dartz). L'erreur ne déroule pas la pile — elle se trouve simplement dans le conteneur, et le niveau suivant décide quoi en faire. Cela rend la propagation plus explicite et contrôlable.
data class HttpResult<out T>(
val data: T?,
val error: AppError?
) {
val isSuccess: Boolean get() = data != null
val isError: Boolean get() = error != null
}
sealed class AppError {
data class Network(val message: String) : AppError()
data class Auth(val message: String) : AppError()
}
fun fetchUser(id: String): HttpResult<User> {
return try {
val response = api.get("/users/$id")
HttpResult(data = parseUser(response), error = null)
} catch (e: IOException) {
HttpResult(data = null, error = AppError.Network("No internet"))
}
}
HttpResult<T> est un conteneur simple avec les champs data et error. La classe scellée AppError définit les types d'erreur (Network, Auth). La fonction fetchUser renvoie HttpResult, la propagation ne nécessite pas de déroulement de la pile — l'appelant vérifie simplement isSuccess/isError. Cette approche est particulièrement utile dans Clean Architecture, où chaque couche (data, domain, presentation) peut transformer l'erreur : IOError -> DomainError -> UiError. La propagation via un conteneur rend ces transformations explicites et testables, contrairement aux exceptions où la chaîne de transformations n'est pas visible dans les signatures des fonctions.
L'une des décisions clés dans la conception de la gestion des erreurs est le choix entre la propagation (transmettre vers le haut) et le traitement (traiter ici). La règle de décision : traitez l'erreur au niveau où il y a suffisamment de contexte pour une action significative. Si vous avez accès à l'interface utilisateur — affichez un message à l'utilisateur. Si vous avez accès au cache — essayez de récupérer. Si vous n'avez ni l'un ni l'autre — propagez.
| Scénario | Action | Justification |
|---|---|---|
| Erreur réseau dans Repository | Propager | Repository ne sait pas si l'utilisateur veut réessayer la requête |
| Erreur d'analyse dans Repository | Traiter (retourner la valeur par défaut) | Repository connaît le format, peut renvoyer une valeur de repli |
| Délai d'attente dans ViewModel | Traiter (UiState.Error) | ViewModel gère UiState, sait comment traduire l'erreur |
| Erreur d'autorisation dans Interceptor | Traiter (rafraîchir le jeton) | Interceptor a accès aux jetons et peut restaurer la session |
| Erreur inconnue dans UseCase | Propager | UseCase n'a pas de contexte UI — seulement la logique métier |
La règle d'or : propagation minimale, traitement maximal aux niveaux inférieurs. Si Repository peut se rétablir à partir du cache — il devrait le faire sans transmettre l'erreur vers le haut. Si ViewModel peut afficher un Snackbar — qu'il l'affiche, sans exiger de code supplémentaire de ViewController. Chaque niveau de propagation augmente le couplage et complique les tests. Selon Google Android Architecture Guide (2026), il est recommandé de minimiser la propagation à travers les limites des couches en utilisant une classe scellée UiState pour représenter tous les états possibles (Loading, Success, Error) au niveau ViewModel, et de ne pas transmettre les exceptions directement à la couche UI.
Une propagation incorrecte est une source de bogues difficiles à trouver dans les applications mobiles. Considérons cinq problèmes principaux auxquels les développeurs sont confrontés et les moyens de les résoudre.
Le problème le plus courant : lors de la propagation, une exception est attrapée, journalisée et une nouvelle est levée sans l'exception d'origine. Le développeur perd la StackTrace et ne peut pas comprendre où exactement l'erreur s'est produite. Dans Swift, utilisez le chaînage d'erreurs : throw MyError(context : originalError). Dans Kotlin : throw AppException(cause = originalException). Dans Dart : throw AppException(message, originalException). Ne créez jamais une nouvelle exception sans transmettre cause/underlyingError.
catch (e : Exception) { /* rien */ } est un antipattern qui fait que l'application continue de fonctionner dans un état incorrect. Si vous êtes sûr que l'erreur peut être ignorée — ajoutez un commentaire avec la justification. Dans Swift, utilisez try? pour ignorer optionnellement (erreur -> nil). Dans Kotlin — Result<T>.onFailure { /* log */ }. N'avalez pas les exceptions sans journalisation.
Si une erreur traverse 5 niveaux ou plus sans être traitée, l'architecture nécessite une révision. Chaque niveau de propagation est une dépendance de la signature throws des fonctions sous-jacentes. Solution : utilisez des conteneurs d'échec (sealed class Result { Success, Error }) aux limites des couches pour rendre la propagation explicite et limitée. Plus la chaîne de propagation est courte, plus il est facile de tester et de déboguer le code.
Dans Kotlin Coroutines, une exception dans launch annule par défaut la coroutine parente et tous les siblings (enfants du même scope). Si une tâche sur 10 échoue, les 9 autres seront annulées, ce qui est souvent indésirable. Utilisez SupervisorJob ou supervisorScope pour isoler les erreurs : une erreur dans un enfant n'annule pas les siblings. ViewModelScope utilise SupervisorJob par défaut, ce qui évite ce problème sous Android.
Dans les API basées sur les callbacks, l'erreur est souvent transmise comme un paramètre du callback. Si le callback ne traite pas l'erreur (ou la traite incorrectement), la propagation devient implicite et se perd facilement. Solution : migrez vers async/await (Swift) ou les coroutines (Kotlin), où la propagation fonctionne via les mécanismes standard de try-catch. Si le callback est inévitable — utilisez Either<Error, T> ou Result<T> pour forcer le traitement des deux cas.
Questions fréquentes
Throw est une action unique de lever une exception. Error Propagation est l'ensemble du processus de transmission d'une erreur à travers plusieurs niveaux de pile, de throw à catch. La propagation inclut throw, la transmission automatique ou manuelle via des fonctions intermédiaires et le traitement final. C'est un concept plus large qui décrit le cycle de vie de l'erreur.
Utilisez des objets mock qui lèvent des exceptions dans des scénarios donnés. Vérifiez que la fonction propage ou traite correctement l'erreur via assertThrows (Kotlin/JUnit) ou XCTAssertThrowsError (Swift/XCTest). Pour la propagation basée sur Result, vérifiez isSuccess/isError et les valeurs dans les deux cas.
La propagation via Result est préférable pour les erreurs attendues (données invalides, règles métier) à l'intérieur d'une limite architecturale. Les exceptions sont meilleures pour les erreurs inattendues (perte de réseau, erreurs d'E/S) qui doivent être traitées à un niveau élevé. Un résultat avec erreur n'interrompt pas le flux d'exécution, une exception l'interrompt.
Dans Kotlin Coroutines, une exception dans launch se propage automatiquement via CoroutineScope avec annulation des siblings. Utilisez supervisorScope ou SupervisorJob pour l'isolation : une erreur dans une coroutine n'annule pas les autres. Pour async, l'erreur doit être traitée explicitement via try-catch lors de l'appel à await(), sinon elle sera avalée.
Le gestionnaire final idéal est la couche UI (ViewController, Fragment/Composable). Seule elle a accès à l'interface utilisateur et peut afficher un message, un Snackbar ou une boîte de dialogue. Les couches intermédiaires (Repository, UseCase, ViewModel) propagent l'erreur, en la transformant si nécessaire en un type de domaine plus abstrait.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi