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 — 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.
Î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”.
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.
Î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.
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.
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ă.
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.
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ă.
| Scenariu | Acțiune | Justificare |
|---|---|---|
| Eroare de rețea în Repository | Propagă | Repository nu știe dacă utilizatorul vrea să reîncerce cererea |
| Eroare de parsare în Repository | Gestionează (returnează valoarea implicită) | Repository cunoaște formatul, poate returna o valoare de fallback |
| Timeout în ViewModel | Gestionează (UiState.Error) | ViewModel gestionează UiState, știe cum să traducă eroarea |
| Eroare de autorizare în Interceptor | Gestionează (reîmprospătare token) | Interceptor are acces la token-uri și poate restabili sesiunea |
| Eroare necunoscută în UseCase | Propagă | 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.
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.
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ă.
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.
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.
Î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.
Î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
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.
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.
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.
Î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ă.
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
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.
Citiți și