Пропагација грешака: шта је то, механизам ширења грешака и како ради у мобилном развоју

Аутор: IT Sectr Објављено: 2026-05-26 Време читања: 9 мин

Пропагација грешака — механизам ширења грешке навише по стеку позива од места настанка до обрађивача. Када функција не може самостално да обради грешку, прослеђује је позивајућој страни кроз изузетак (exception), throws декларацију или тип повратне вредности. Правилна имплементација пропагације је критична за стабилност мобилних апликација: необрађене или некоректно прослеђене грешке доводе до падова. Према Apple Swift Documentation (2026), аутоматска пропагација кроз throws у Swift-у омогућава прослеђивање грешке на било који ниво без boilerplate кода.

Главно

  • Пропагација грешака — пренос грешке од места настанка навише по стеку до обрађивача, прескачући посредне функције
  • Аутоматска пропагација кроз throws у Swift-у прослеђује грешку без експлицитног кода на сваком нивоу стек
  • Ручна пропагација у Kotlin-у и Dart-у захтева експлицитан try-catch или пренос у Result контејнеру на сваком нивоу
  • Checked exceptions у Java-и приморавају на пропагацију кроз throws у потпису, unchecked — омогућавају игнорисање
  • Result тип — алтернатива изузецима, где се грешка прослеђује као вредност без одмотавања стека

Шта је пропагација грешака?

Пропагација грешака — процес преноса објекта грешке од функције у којој је настала навише кроз ланац позива до најближег одговарајућег обрађивача. Замислите стек позива: 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-сличних контејнера (за очекиване). Важно је разумети: пропагација није циљ, већ нужност. Идеална архитектура минимизира дубину пропагације, обрађујући грешке на најнижем могућем нивоу где има довољно контекста за доношење одлуке.

Пропагација кроз Throws у Swift-у

У Swift-у пропагација кроз throws се одвија аутоматски: ако функција A са throws позива функцију B са throws, и A не обрађује грешку из B у do-catch, грешка се аутоматски прослеђује позивајућој страни A. Ово елиминише boilerplate код карактеристичан за Java checked exceptions, где throws мора да се декларише у свакој методи ланца. Swift користи принцип „једна throws функција у ланцу = цео ланац постаје throws, ако се не обрађује на посредним нивоима”.

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 — крајњи обрађивач
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-у

У Kotlin-у пропагација кроз изузетке не захтева декларацију throws у потпису (сви изузеци су unchecked). Изузетак аутоматски путује навише по стеку док не наиђе на try-catch. Међутим, одсуство throws у потпису чини пропагацију имплицитном: програмер из потписа функције не види да она може да баци изузетак. Ово је и предност (мање boilerplate) и мана (лакше заборавити да се обради). Kotlin решава овај проблем кроз конвенције и архитектонске обрасце, а не кроз језик.

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 тип

Алтернатива изузецима — пропагација кроз тип-контејнер који преноси успех или грешку као вредност. У овом приступу функција не враћа вредност, већ омот: Result<T, E> у Swift-у, Result<T> у Kotlin-у, Either<L, R> у Dart-у (из пакета fpdart или dartz). Грешка не одмотава стек — једноставно лежи у контејнеру, а следећи ниво одлучује шта ће са њом. Ово чини пропагацију експлицитнијом и контролисанијом.

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> — једноставан контејнер са пољима data и error. Sealed class AppError дефинише типове грешака (Network, Auth). Функција fetchUser враћа HttpResult, пропагација не захтева одмотавање стека — позивајућа страна једноставно проверава isSuccess/isError. Овај приступ је посебно користан у Clean Architecture, где сваки слој (data, domain, presentation) може да трансформише грешку: IOError -> DomainError -> UiError. Пропагација кроз контејнер чини ове трансформације експлицитним и тестирабилним, за разлику од изузетака, где ланац трансформација није видљив у потписима функција.

Пропагација vs Обрада: када проследити, када обрадити

Једна од кључних одлука у пројектовању обраде грешака је избор између пропагације (прослеђивање горе) и обраде (обрада овде). Правило доношења одлуке: обради грешку на оном нивоу где има довољно контекста за смислену акцију. Ако имаш приступ 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)

catch (e: Exception) { /* ништа */ } — антиобразац који доводи до тога да апликација наставља рад у некоректном стању. Ако сте сигурни да грешка може да се игнорише — додајте коментар са образложењем. У Swift-у за опционо игнорисање користите try? (грешка -> nil). У Kotlin-у — Result<T>.onFailure { /* log */ }. Не гушите изузетке без логовања.

Претерана дубина пропагације

Ако грешка пролази кроз 5+ нивоа без обраде, архитектура захтева преиспитивање. Сваки ниво пропагације је зависност од throws потписа нижих функција. Решење: користите Failure контејнере (sealed class Result { Success, Error }) на границама слојева да би пропагација била експлицитна и ограничена. Што је ланац пропагације краћи, лакше је тестирати и отклањати грешке у коду.

Пропагација у корутинама без SupervisorJob-а

У Kotlin Coroutines, изузетак у launch-у подразумевано отказује родитељску корутину и све siblings (децу истог scope-а). Ако један од 10 паралелних задатака падне, преосталих 9 ће бити отказано, што је често непожељно. Користите SupervisorJob или supervisorScope за изолацију грешака: грешка у једном child-у не отказује siblings. ViewModelScope подразумевано користи SupervisorJob, што штити од овог проблема у Android-у.

Пропагација кроз callback без обраде

У callback-базираном API-ју, грешка се често прослеђује као параметар callback-а. Ако callback не обрађује грешку (или је обрађује некоректно), пропагација постаје имплицитна и лако се губи. Решење: мигрирајте на async/await (Swift) или корутине (Kotlin), где пропагација ради кроз стандардне механизме try-catch. Ако је callback неизбежан — користите Either<Error, T> или Result<T> за принудну обраду оба случаја.

Често постављана питања

Чим се пропагација грешака разликује од throw?

Throw је једнократна радња бацања изузетка. Пропагација грешака је цео процес преноса грешке кроз више нивоа стека, од throw до catch. Пропагација укључује throw, аутоматски или ручни пренос кроз посредне функције и коначну обраду. То је шири концепт који описује животни циклус грешке.

Како тестирати пропагацију грешака?

Користите mock објекте који бацају изузетке у задатим сценаријима. Проверавајте да функција коректно пропагира или обрађује грешку кроз assertThrows (Kotlin/JUnit) или XCTAssertThrowsError (Swift/XCTest). За Result-базирану пропагацију проверавајте isSuccess/isError и вредности у оба случаја.

Када је пропагација кроз Result боља од изузетака?

Result пропагација је пожељнија за очекиване грешке (неисправни подаци, пословна правила) у оквиру једне архитектонске границе. Изузеци су бољи за неочекиване грешке (губитак мреже, I/O грешке) које треба обрадити на високом нивоу. Резултат са грешком не прекида ток извршења, изузетак прекида.

Како пропагирати грешку кроз Kotlin корутине?

У Kotlin Coroutines, изузетак у launch-у се аутоматски пропагира кроз CoroutineScope са отказивањем siblings. Користите supervisorScope или SupervisorJob за изолацију: грешка у једној корутини не отказује остале. За async, грешка се мора експлицитно обрадити кроз try-catch при позиву await(), иначе ће бити прогутана.

Који ниво треба да буде крајњи обрађивач грешке?

Идеалан крајњи обрађивач је UI слој (ViewController, Fragment/Composable). Само он има приступ корисничком интерфејсу и може да прикаже поруку, Snackbar или дијалог. Посредни слојеви (Repository, UseCase, ViewModel) пропагирају грешку, трансформишући је по потреби у апстрактнији доменски тип.

Закључак

  • Пропагација грешака — пренос грешке навише по стеку од места настанка до обрађивача кроз изузетке или Result контејнере
  • Аутоматска пропагација у Swift-у кроз throws не захтева код на посредним нивоима — грешка сама путује навише
  • Ручна пропагација у Kotlin-у кроз експлицитан try-catch и throw на сваком нивоу чини обраду свесном, али опширном
  • Result тип преноси грешку као вредност без одмотавања стека, згодан за очекиване грешке у Clean Architecture
  • Правило обраде: обради на нивоу са контекстом (UI), пропагирај кроз нивое без контекста (domain, data)
  • Антиобрасци: празан catch, губитак узрока при трансформацији, претерана дубина пропагације, игнорисање SupervisorJob-а
  • Пројектујте типизиране грешке (sealed class / enum) за сваки слој и трансформишите их при преласку граница слојева

Развићемо мобилну апликацију под кључ

IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође