Error Propagation : ce que c'est, mécanisme de propagation des erreurs et comment ça fonctionne dans le développement mobile

Auteur : IT Sectr Publié le : 2026-05-26 Temps de lecture : 9 min

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 — transmission d'une erreur du lieu d'origine vers le haut de la pile jusqu'à un gestionnaire, en contournant les fonctions intermédiaires
  • Propagation automatique via throws dans Swift transmet l'erreur sans code explicite à chaque niveau de la pile
  • Propagation manuelle dans Kotlin et Dart nécessite un try-catch explicite ou une transmission dans un conteneur Result à chaque niveau
  • Exceptions vérifiées en Java imposent la propagation via throws dans la signature, les non vérifiées peuvent être ignorées
  • Result Type — une alternative aux exceptions où l'erreur est transmise comme une valeur sans déroulement de la pile

Qu'est-ce que Error Propagation ?

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.

Propagation via Throws dans Swift

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 ».

swift
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.

Propagation via les exceptions dans Kotlin

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.

kotlin
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.

Propagation via Result Type

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.

kotlin
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.

Propagation vs Traitement : quand transmettre, quand traiter

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énarioActionJustification
Erreur réseau dans RepositoryPropagerRepository ne sait pas si l'utilisateur veut réessayer la requête
Erreur d'analyse dans RepositoryTraiter (retourner la valeur par défaut)Repository connaît le format, peut renvoyer une valeur de repli
Délai d'attente dans ViewModelTraiter (UiState.Error)ViewModel gère UiState, sait comment traduire l'erreur
Erreur d'autorisation dans InterceptorTraiter (rafraîchir le jeton)Interceptor a accès aux jetons et peut restaurer la session
Erreur inconnue dans UseCasePropagerUseCase 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.

Problèmes et antipatterns de Error Propagation

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.

Perte du contexte de l'erreur

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.

Ignorer l'erreur (catch vide)

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.

Profondeur excessive de propagation

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.

Propagation dans les coroutines sans SupervisorJob

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.

Propagation via callback sans traitement

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

En quoi Error Propagation diffère-t-elle de throw ?

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.

Comment tester Error Propagation ?

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.

Quand la propagation via Result est-elle meilleure que les exceptions ?

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.

Comment propager une erreur via les coroutines Kotlin ?

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.

Quel niveau devrait être le gestionnaire d'erreur final ?

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é

  • Error Propagation — transmission d'une erreur vers le haut de la pile du lieu d'origine à un gestionnaire via des exceptions ou des conteneurs Result
  • Propagation automatique dans Swift via throws ne nécessite pas de code aux niveaux intermédiaires — l'erreur remonte d'elle-même
  • Propagation manuelle dans Kotlin via try-catch explicite et throw à chaque niveau rend le traitement conscient mais verbeux
  • Result Type transmet l'erreur comme une valeur sans déroulement de la pile, pratique pour les erreurs attendues dans Clean Architecture
  • Règle de traitement : traiter au niveau avec contexte (UI), propager à travers les niveaux sans contexte (domain, data)
  • Antipatterns : catch vide, perte de cause lors de la transformation, profondeur excessive de propagation, ignorance de SupervisorJob
  • Concevoir des erreurs typées (sealed class / enum) pour chaque couche et les transformer lors du franchissement des limites de couches

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.

Discuter du projet

Lisez aussi