Пропагација грешака — механизам ширења грешке навише по стеку позива од места настанка до обрађивача. Када функција не може самостално да обради грешку, прослеђује је позивајућој страни кроз изузетак (exception), throws декларацију или тип повратне вредности. Правилна имплементација пропагације је критична за стабилност мобилних апликација: необрађене или некоректно прослеђене грешке доводе до падова. Према Apple Swift Documentation (2026), аутоматска пропагација кроз throws у Swift-у омогућава прослеђивање грешке на било који ниво без boilerplate кода.
Главно
Пропагација грешака — процес преноса објекта грешке од функције у којој је настала навише кроз ланац позива до најближег одговарајућег обрађивача. Замислите стек позива: ViewController позива ViewModel, ViewModel позива Repository, Repository позива API. Ако API врати грешку мреже, она мора да прође кроз Repository и ViewModel до ViewController-а, који ће приказати поруку кориснику. Свака посредна функција одлучује: обрадити грешку или проследити даље (пропагирати).
Постоје два приступа пропагацији: аутоматски и ручни. Код аутоматског приступа (Swift throws, Java checked exceptions) компајлер приморава програмера или да обради грешку или да декларише пропагацију у потпису. Код ручног приступа (Result тип, Kotlin Try) грешка се прослеђује као вредност — програмер експлицитно пише код за пренос или трансформацију грешке. Према Kotlin Result Docs (2026), Result<T> у Kotlin-у није намењен за директну пропагацију кроз границе функција — мора се трансформисати или обрадити на сваком нивоу, што чини пропагацију свеснијом, али и опширнијом.
Избор приступа зависи од архитектуре апликације и језика. У Swift-у доминира аутоматска пропагација кроз throws, у Kotlin-у — мешавина изузетака (за неочекиване грешке) и Result-сличних контејнера (за очекиване). Важно је разумети: пропагација није циљ, већ нужност. Идеална архитектура минимизира дубину пропагације, обрађујући грешке на најнижем могућем нивоу где има довољно контекста за доношење одлуке.
У Swift-у пропагација кроз throws се одвија аутоматски: ако функција A са throws позива функцију B са throws, и A не обрађује грешку из B у do-catch, грешка се аутоматски прослеђује позивајућој страни A. Ово елиминише boilerplate код карактеристичан за Java checked exceptions, где throws мора да се декларише у свакој методи ланца. Swift користи принцип „једна throws функција у ланцу = цео ланац постаје throws, ако се не обрађује на посредним нивоима”.
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 — крајњи обрађивач
func onButtonTap() {
let vm = UserViewModel()
do {
let user = try vm.loadUser(id: 42)
updateUI(user)
} catch {
showError("Учитавање корисника није успело")
}
}
Ланац пропагације: networkService.request -> fetchUser -> loadUser -> onButtonTap. Свака посредна функција је означена throws и не садржи do-catch — грешка се аутоматски прослеђује навише. ViewController onButtonTap је крајњи обрађивач са do-catch. Ако би ViewModel одлучио да трансформише грешку (умота у други тип), могао би да користи do-catch и нови throw. Аутоматска пропагација скраћује код: Repository не мора да зна како да обради грешку — то је одговорност ViewController-а, који има приступ UI-ју за приказ поруке кориснику.
У Kotlin-у пропагација кроз изузетке не захтева декларацију throws у потпису (сви изузеци су unchecked). Изузетак аутоматски путује навише по стеку док не наиђе на try-catch. Међутим, одсуство throws у потпису чини пропагацију имплицитном: програмер из потписа функције не види да она може да баци изузетак. Ово је и предност (мање boilerplate) и мана (лакше заборавити да се обради). 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")
}
}
}
}
У Repository-ју пропагација са трансформацијом: при IOException (мрежа недоступна) функција покушава да добије кеширане податке из базе података. Ако је кеш празан, баца AppException — пропагација се наставља са новим типом грешке. ViewModel хвата AppException и преводи га у UiState.Error — грешка не иде даље, пропагација је завршена на нивоу UI слоја. Kotlin Coroutines додају посебности: изузеци у launch-у се аутоматски шире кроз CoroutineExceptionHandler, а у async-у — само при позиву await(). Ово је важно узети у обзир при пројектовању пропагације у корутинама — SupervisorJob спречава отказивање родитељске корутине при грешци у детету.
Алтернатива изузецима — пропагација кроз тип-контејнер који преноси успех или грешку као вредност. У овом приступу функција не враћа вредност, већ омот: Result<T, E> у Swift-у, Result<T> у Kotlin-у, Either<L, R> у Dart-у (из пакета fpdart или dartz). Грешка не одмотава стек — једноставно лежи у контејнеру, а следећи ниво одлучује шта ће са њом. Ово чини пропагацију експлицитнијом и контролисанијом.
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> — једноставан контејнер са пољима data и error. Sealed class AppError дефинише типове грешака (Network, Auth). Функција fetchUser враћа HttpResult, пропагација не захтева одмотавање стека — позивајућа страна једноставно проверава isSuccess/isError. Овај приступ је посебно користан у Clean Architecture, где сваки слој (data, domain, presentation) може да трансформише грешку: IOError -> DomainError -> UiError. Пропагација кроз контејнер чини ове трансформације експлицитним и тестирабилним, за разлику од изузетака, где ланац трансформација није видљив у потписима функција.
Једна од кључних одлука у пројектовању обраде грешака је избор између пропагације (прослеђивање горе) и обраде (обрада овде). Правило доношења одлуке: обради грешку на оном нивоу где има довољно контекста за смислену акцију. Ако имаш приступ UI-ју — прикажи поруку кориснику. Ако имаш приступ кешу — покушај да се опоравиш. Ако немаш ни једно ни друго — пропагирај.
| Сценарио | Акција | Образложење |
|---|---|---|
| Грешка мреже у Repository-ју | Пропагирај | Repository не зна да ли корисник жели да понови захтев |
| Грешка парсирања у Repository-ју | Обради (врати подразумевану вредност) | Repository зна формат, може да врати fallback вредност |
| Тајмаут у ViewModel-у | Обради (UiState.Error) | ViewModel управља UiState-ом, зна како да преведе грешку |
| Грешка ауторизације у Interceptor-у | Обради (освежи токен) | Interceptor има приступ токенима и може да обнови сесију |
| Непозната грешка у UseCase-у | Пропагирај | UseCase нема UI контекст — само пословна логика |
Златно правило: минимум пропагације, максимум обраде на нижим нивоима. Ако Repository може да се опорави из кеша — треба да то уради, не прослеђујући грешку горе. Ако ViewModel може да прикаже Snackbar — нека га прикаже, не захтевајући додатни код од ViewController-а. Сваки ниво пропагације повећава повезаност и отежава тестирање. Према Google Android Architecture Guide (2026), препоручује се минимализовање пропагације кроз границе слојева, користећи sealed class UiState за представљање свих могућих стања (Loading, Success, Error) на нивоу ViewModel-а, и не прослеђивање изузетака директно у UI слој.
Некоректна пропагација је извор тешко уочљивих грешака у мобилним апликацијама. Размотримо пет главних проблема са којима се сусрећу програмери и начине њиховог решавања.
Најчешћи проблем: при пропагацији, изузетак се хвата, логује и баца нови без оригиналног изузетка. Програмер губи StackTrace и не може да разуме где је тачно настала грешка. У Swift-у користите ланчање грешака: throw MyError(context: originalError). У Kotlin-у: throw AppException(cause = originalException). У Dart-у: throw AppException(message, originalException). Никада не стварајте нови изузетак без преноса узрока/основне грешке.
catch (e: Exception) { /* ништа */ } — антиобразац који доводи до тога да апликација наставља рад у некоректном стању. Ако сте сигурни да грешка може да се игнорише — додајте коментар са образложењем. У Swift-у за опционо игнорисање користите try? (грешка -> nil). У Kotlin-у — Result<T>.onFailure { /* log */ }. Не гушите изузетке без логовања.
Ако грешка пролази кроз 5+ нивоа без обраде, архитектура захтева преиспитивање. Сваки ниво пропагације је зависност од throws потписа нижих функција. Решење: користите Failure контејнере (sealed class Result { Success, Error }) на границама слојева да би пропагација била експлицитна и ограничена. Што је ланац пропагације краћи, лакше је тестирати и отклањати грешке у коду.
У Kotlin Coroutines, изузетак у launch-у подразумевано отказује родитељску корутину и све siblings (децу истог scope-а). Ако један од 10 паралелних задатака падне, преосталих 9 ће бити отказано, што је често непожељно. Користите SupervisorJob или supervisorScope за изолацију грешака: грешка у једном child-у не отказује siblings. ViewModelScope подразумевано користи SupervisorJob, што штити од овог проблема у Android-у.
У callback-базираном API-ју, грешка се често прослеђује као параметар callback-а. Ако callback не обрађује грешку (или је обрађује некоректно), пропагација постаје имплицитна и лако се губи. Решење: мигрирајте на async/await (Swift) или корутине (Kotlin), где пропагација ради кроз стандардне механизме try-catch. Ако је callback неизбежан — користите Either<Error, T> или Result<T> за принудну обраду оба случаја.
Често постављана питања
Throw је једнократна радња бацања изузетка. Пропагација грешака је цео процес преноса грешке кроз више нивоа стека, од throw до catch. Пропагација укључује throw, аутоматски или ручни пренос кроз посредне функције и коначну обраду. То је шири концепт који описује животни циклус грешке.
Користите mock објекте који бацају изузетке у задатим сценаријима. Проверавајте да функција коректно пропагира или обрађује грешку кроз assertThrows (Kotlin/JUnit) или XCTAssertThrowsError (Swift/XCTest). За Result-базирану пропагацију проверавајте isSuccess/isError и вредности у оба случаја.
Result пропагација је пожељнија за очекиване грешке (неисправни подаци, пословна правила) у оквиру једне архитектонске границе. Изузеци су бољи за неочекиване грешке (губитак мреже, I/O грешке) које треба обрадити на високом нивоу. Резултат са грешком не прекида ток извршења, изузетак прекида.
У Kotlin Coroutines, изузетак у launch-у се аутоматски пропагира кроз CoroutineScope са отказивањем siblings. Користите supervisorScope или SupervisorJob за изолацију: грешка у једној корутини не отказује остале. За async, грешка се мора експлицитно обрадити кроз try-catch при позиву await(), иначе ће бити прогутана.
Идеалан крајњи обрађивач је UI слој (ViewController, Fragment/Composable). Само он има приступ корисничком интерфејсу и може да прикаже поруку, Snackbar или дијалог. Посредни слојеви (Repository, UseCase, ViewModel) пропагирају грешку, трансформишући је по потреби у апстрактнији доменски тип.
Закључак
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође