Propagarea erorilor: ce este, mecanismul de răspândire a erorilor și cum funcționează în dezvoltarea mobilă

Autor: IT Sectr Publicat: 2026-05-26 Timp de citire: 9 min

Propagarea erorilor — mecanismul de răspândire a erorii în sus pe stiva de apeluri de la locul apariției până la handler. Când o funcție nu poate gestiona eroarea singură, o transmite părții apelante prin excepție (exception), declarația throws sau tipul de returnare. Implementarea corectă a propagării este critică pentru stabilitatea aplicațiilor mobile: erorile negestionate sau transmise incorect duc la crash-uri. Conform Apple Swift Documentation (2026), propagarea automată prin throws în Swift permite transmiterea erorii la orice nivel fără cod boilerplate.

Principalele idei

  • Propagarea erorilor — transmiterea erorii de la locul apariției în sus pe stivă către handler, ocolind funcțiile intermediare
  • Propagarea automată prin throws în Swift transmite eroarea fără cod explicit la fiecare nivel al stivei
  • Propagarea manuală în Kotlin și Dart necesită try-catch explicit sau transmitere în containerul Result la fiecare nivel
  • Excepțiile checked în Java forțează propagarea prin throws în semnătură, cele unchecked permit ignorarea
  • Tipul Result — alternativă la excepții, unde eroarea este transmisă ca valoare fără derularea stivei

Ce este propagarea erorilor?

Propagarea erorilor — procesul de transmitere a obiectului de eroare de la funcția unde a apărut în sus pe lanțul de apeluri până la cel mai apropiat handler potrivit. Imaginați-vă stiva de apeluri: ViewController apelează ViewModel, ViewModel apelează Repository, Repository apelează API. Dacă API returnează o eroare de rețea, aceasta trebuie să treacă prin Repository și ViewModel către ViewController, care va afișa un mesaj utilizatorului. Fiecare funcție intermediară decide: să gestioneze eroarea sau să o transmită mai departe (să o propage).

Există două abordări ale propagării: automată și manuală. În abordarea automată (Swift throws, Java checked exceptions) compilatorul forțează dezvoltatorul fie să gestioneze eroarea, fie să declare propagarea în semnătură. În abordarea manuală (tipul Result, Kotlin Try) eroarea este transmisă ca valoare — dezvoltatorul scrie explicit cod pentru transmiterea sau transformarea erorii. Conform Kotlin Result Docs (2026), Result<T> în Kotlin nu este destinat propagării directe peste granițele funcțiilor — trebuie transformat sau gestionat la fiecare nivel, ceea ce face propagarea mai conștientă, dar și mai verbose.

Alegerea abordării depinde de arhitectura aplicației și de limbaj. În Swift domină propagarea automată prin throws, în Kotlin — un mix de excepții (pentru erori neașteptate) și containere de tip Result (pentru erori așteptate). Este important să înțelegeți: propagarea nu este un scop, ci o necesitate. Arhitectura ideală minimizează adâncimea propagării, gestionând erorile la cel mai jos nivel posibil, unde există suficient context pentru a lua o decizie.

Propagarea prin Throws în Swift

În Swift propagarea prin throws are loc automat: dacă funcția A cu throws apelează funcția B cu throws, iar A nu gestionează eroarea lui B în do-catch, eroarea este transmisă automat părții apelante a lui A. Acest lucru elimină codul boilerplate caracteristic excepțiilor checked din Java, unde throws trebuie declarat în fiecare metodă a lanțului. Swift folosește principiul „o funcție throws în lanț = întregul lanț devine throws, dacă nu se gestionează la nivelurile intermediare”.

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 — handler final
func onButtonTap() {
    let vm = UserViewModel()
    do {
        let user = try vm.loadUser(id: 42)
        updateUI(user)
    } catch {
        showError("Încărcarea utilizatorului a eșuat")
    }
}

Lanțul de propagare: networkService.request -> fetchUser -> loadUser -> onButtonTap. Fiecare funcție intermediară este marcată cu throws și nu conține do-catch — eroarea este transmisă automat în sus. ViewController onButtonTap este handlerul final cu do-catch. Dacă ViewModel ar decide să transforme eroarea (să o împacheteze într-un alt tip), ar putea folosi do-catch și un nou throw. Propagarea automată scurtează codul: Repository nu trebuie să știe cum să gestioneze eroarea — aceasta este responsabilitatea ViewController, care are acces la UI pentru a afișa un mesaj utilizatorului.

Propagarea prin excepții în Kotlin

În Kotlin propagarea prin excepții nu necesită declarația throws în semnătură (toate excepțiile sunt unchecked). Excepția urcă automat pe stivă până când întâlnește un try-catch. Cu toate acestea, absența throws în semnătură face propagarea implicită: dezvoltatorul nu vede din semnătura funcției că aceasta poate arunca o excepție. Acesta este atât un avantaj (mai puțin boilerplate) cât și un dezavantaj (mai ușor de uitat să gestionezi). Kotlin rezolvă această problemă prin convenții și pattern-uri arhitecturale, nu prin limbaj.

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")
            }
        }
    }
}

În Repository propagarea cu transformare: la IOException (rețea indisponibilă) funcția încearcă să obțină date din cache din baza de date. Dacă cache-ul este gol, aruncă AppException — propagarea continuă cu un nou tip de eroare. ViewModel prinde AppException și îl traduce în UiState.Error — eroarea nu merge mai departe, propagarea se încheie la nivelul stratului UI. Kotlin Coroutines adaugă particularități: excepțiile în launch se propagă automat prin CoroutineExceptionHandler, iar în async — doar la apelarea await(). Este important să țineți cont de acest lucru la proiectarea propagării în corutine — SupervisorJob previne anularea corutinei părinte la o eroare în corutina copil.

Propagarea prin tipul Result

Alternativa la excepții — propagarea printr-un tip-container care transmite succesul sau eroarea ca valoare. În această abordare, funcția returnează nu o valoare, ci o împachetare: Result<T, E> în Swift, Result<T> în Kotlin, Either<L, R> în Dart (din pachetul fpdart sau dartz). Eroarea nu derulează stiva — pur și simplu stă în container, iar nivelul următor decide ce să facă cu ea. Acest lucru face propagarea mai explicită și mai controlabilă.

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> — un container simplu cu câmpurile data și error. Sealed class AppError definește tipurile de erori (Network, Auth). Funcția fetchUser returnează HttpResult, propagarea nu necesită derularea stivei — partea apelantă verifică pur și simplu isSuccess/isError. Această abordare este deosebit de utilă în Clean Architecture, unde fiecare strat (data, domain, presentation) poate transforma eroarea: IOError -> DomainError -> UiError. Propagarea prin container face aceste transformări explicite și testabile, spre deosebire de excepții, unde lanțul de transformări nu este vizibil în semnăturile funcțiilor.

Propagare vs Gestionare: când să transmiți, când să gestionezi

Una dintre deciziile cheie în proiectarea gestionării erorilor este alegerea între propagare (transmitere mai sus) și gestionare (gestionare aici). Regula de decizie: gestionează eroarea la nivelul unde există suficient context pentru o acțiune semnificativă. Dacă ai acces la UI — afișează un mesaj utilizatorului. Dacă ai acces la cache — încearcă să recuperezi. Dacă nu ai nici una, nici alta — propagă.

ScenariuAcțiuneJustificare
Eroare de rețea în RepositoryPropagăRepository nu știe dacă utilizatorul vrea să reîncerce cererea
Eroare de parsare în RepositoryGestionează (returnează valoarea implicită)Repository cunoaște formatul, poate returna o valoare de fallback
Timeout în ViewModelGestionează (UiState.Error)ViewModel gestionează UiState, știe cum să traducă eroarea
Eroare de autorizare în InterceptorGestionează (reîmprospătare token)Interceptor are acces la token-uri și poate restabili sesiunea
Eroare necunoscută în UseCasePropagăUseCase nu are context UI — doar logică de business

Regula de aur: minim de propagare, maxim de gestionare la nivelurile inferioare. Dacă Repository se poate recupera din cache — ar trebui să o facă, fără a transmite eroarea mai sus. Dacă ViewModel poate afișa un Snackbar — să îl afișeze, fără a necesita cod suplimentar de la ViewController. Fiecare nivel de propagare crește cuplarea și complică testarea. Conform Google Android Architecture Guide (2026), se recomandă minimizarea propagării peste granițele straturilor, folosind sealed class UiState pentru a reprezenta toate stările posibile (Loading, Success, Error) la nivelul ViewModel și a nu transmite excepții direct în stratul UI.

Probleme și anti-pattern-uri ale propagării erorilor

Propagarea incorectă este o sursă de bug-uri greu de depistat în aplicațiile mobile. Să examinăm cinci probleme principale cu care se confruntă dezvoltatorii și modalitățile de rezolvare a acestora.

Pierderea contextului erorii

Cea mai frecventă problemă: la propagare, excepția este interceptată, logată și se aruncă una nouă fără excepția originală. Dezvoltatorul pierde StackTrace și nu poate înțelege unde exact a apărut eroarea. În Swift folosiți lanțuirea erorilor: throw MyError(context: originalError). În Kotlin: throw AppException(cause = originalException). În Dart: throw AppException(message, originalException). Nu creați niciodată o excepție nouă fără a transmite cauza/eroarea subiacentă.

Ignorarea erorii (catch gol)

catch (e: Exception) { /* nimic */ } — anti-pattern care duce la continuarea funcționării aplicației într-o stare incorectă. Dacă sunteți sigur că eroarea poate fi ignorată — adăugați un comentariu cu justificarea. În Swift pentru ignorarea opțională folosiți try? (eroare -> nil). În Kotlin — Result<T>.onFailure { /* log */ }. Nu înăbușiți excepțiile fără logare.

Adâncimea excesivă a propagării

Dacă eroarea trece prin 5+ niveluri fără gestionare, arhitectura necesită revizuire. Fiecare nivel de propagare este o dependență de semnătura throws a funcțiilor subiacente. Soluție: folosiți containere de tip Failure (sealed class Result { Success, Error }) la granițele straturilor pentru a face propagarea explicită și limitată. Cu cât lanțul de propagare este mai scurt, cu atât este mai ușor să testați și să depanați codul.

Propagarea în corutine fără SupervisorJob

În Kotlin Coroutines, excepția în launch în mod implicit anulează corutina părinte și toți siblings (copiii aceluiași scope). Dacă una din 10 sarcini paralele eșuează, celelalte 9 vor fi anulate, ceea ce este adesea nedorit. Folosiți SupervisorJob sau supervisorScope pentru izolarea erorilor: o eroare într-un child nu anulează siblings. ViewModelScope folosește SupervisorJob în mod implicit, ceea ce protejează de această problemă în Android.

Propagarea prin callback fără gestionare

În API-urile bazate pe callback, eroarea este adesea transmisă ca parametru al callback-ului. Dacă callback-ul nu gestionează eroarea (sau o gestionează incorect), propagarea devine implicită și se pierde ușor. Soluție: migrați la async/await (Swift) sau corutine (Kotlin), unde propagarea funcționează prin mecanismele standard try-catch. Dacă callback-ul este inevitabil — folosiți Either<Error, T> sau Result<T> pentru gestionarea forțată a ambelor cazuri.

Întrebări frecvente

Cu ce diferă propagarea erorilor de throw?

Throw este o acțiune unică de aruncare a excepției. Propagarea erorilor este întregul proces de transmitere a erorii prin mai multe niveluri ale stivei, de la throw la catch. Propagarea include throw, transmiterea automată sau manuală prin funcții intermediare și gestionarea finală. Este un concept mai larg care descrie ciclul de viață al erorii.

Cum să testați propagarea erorilor?

Folosiți obiecte mock care aruncă excepții în scenarii date. Verificați că funcția propagă sau gestionează corect eroarea prin assertThrows (Kotlin/JUnit) sau XCTAssertThrowsError (Swift/XCTest). Pentru propagarea bazată pe Result, verificați isSuccess/isError și valorile în ambele cazuri.

Când este propagarea prin Result mai bună decât excepțiile?

Propagarea prin Result este preferată pentru erorile așteptate (date incorecte, reguli de business) în cadrul unei granițe arhitecturale. Excepțiile sunt mai bune pentru erorile neașteptate (pierderea rețelei, erori de intrare/ieșire) care trebuie gestionate la nivel înalt. Rezultatul cu eroare nu întrerupe fluxul de execuție, excepția îl întrerupe.

Cum să propagați o eroare prin corutine Kotlin?

În Kotlin Coroutines, excepția în launch se propagă automat prin CoroutineScope cu anularea siblings. Folosiți supervisorScope sau SupervisorJob pentru izolare: o eroare într-o corutină nu anulează celelalte. Pentru async, eroarea trebuie gestionată explicit prin try-catch la apelarea await(), altfel va fi înghițită.

Care nivel ar trebui să fie handlerul final al erorii?

Handlerul final ideal este stratul UI (ViewController, Fragment/Composable). Numai acesta are acces la interfața utilizatorului și poate afișa un mesaj, Snackbar sau dialog. Straturile intermediare (Repository, UseCase, ViewModel) propagă eroarea, transformând-o la nevoie într-un tip de domeniu mai abstract.

Concluzii

  • Propagarea erorilor — transmiterea erorii în sus pe stivă de la locul apariției la handler prin excepții sau containere Result
  • Propagarea automată în Swift prin throws nu necesită cod la nivelurile intermediare — eroarea urcă singură
  • Propagarea manuală în Kotlin prin try-catch și throw explicit la fiecare nivel face gestionarea conștientă, dar verbose
  • Tipul Result transmite eroarea ca valoare fără derularea stivei, util pentru erori așteptate în Clean Architecture
  • Regula de gestionare: gestionează la nivelul cu context (UI), propagă prin nivelurile fără context (domain, data)
  • Anti-pattern-uri: catch gol, pierderea cauzei la transformare, adâncime excesivă a propagării, ignorarea SupervisorJob
  • Proiectați erori tipizate (sealed class / enum) pentru fiecare strat și transformați-le la traversarea granițelor straturilor

Vom dezvolta o aplicație mobilă la cheie

IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.

Discutați proiectul

Citiți și