Propagasi Kesalahan: apa itu, mekanisme penyebaran kesalahan dan cara kerjanya dalam pengembangan mobile

Penulis: IT Sectr Diterbitkan: 2026-05-26 Waktu membaca: 9 mnt

Propagasi Kesalahan — mekanisme penyebaran kesalahan ke atas melalui tumpukan panggilan dari tempat terjadinya hingga penangan. Ketika suatu fungsi tidak dapat menangani kesalahan secara mandiri, ia meneruskannya ke pihak pemanggil melalui exception, deklarasi throws, atau tipe kembalian. Implementasi propagasi yang benar sangat penting untuk stabilitas aplikasi mobile: kesalahan yang tidak ditangani atau diteruskan secara tidak benar menyebabkan crash. Menurut Apple Swift Documentation (2026), propagasi otomatis melalui throws di Swift memungkinkan penerusan kesalahan ke tingkat mana pun tanpa kode boilerplate.

Poin Utama

  • Propagasi Kesalahan — penerusan kesalahan dari tempat terjadinya ke atas melalui tumpukan ke penangan, melewati fungsi perantara
  • Propagasi otomatis melalui throws di Swift meneruskan kesalahan tanpa kode eksplisit di setiap tingkat tumpukan
  • Propagasi manual di Kotlin dan Dart memerlukan try-catch eksplisit atau penerusan dalam wadah Result di setiap tingkat
  • Checked exceptions di Java memaksa propagasi melalui throws di tanda tangan, unchecked — memungkinkan untuk diabaikan
  • Tipe Result — alternatif untuk exception, di mana kesalahan diteruskan sebagai nilai tanpa membuka tumpukan

Apa itu Propagasi Kesalahan?

Propagasi Kesalahan — proses penerusan objek kesalahan dari fungsi tempat terjadinya ke atas melalui rantai panggilan ke penangan terdekat yang sesuai. Bayangkan tumpukan panggilan: ViewController memanggil ViewModel, ViewModel memanggil Repository, Repository memanggil API. Jika API mengembalikan kesalahan jaringan, ia harus melewati Repository dan ViewModel ke ViewController, yang akan menampilkan pesan kepada pengguna. Setiap fungsi perantara memutuskan: menangani kesalahan atau meneruskannya (mempropagasi).

Ada dua pendekatan untuk propagasi: otomatis dan manual. Dalam pendekatan otomatis (Swift throws, Java checked exceptions) kompiler memaksa pengembang untuk menangani kesalahan atau mendeklarasikan propagasi di tanda tangan. Dalam pendekatan manual (tipe Result, Kotlin Try) kesalahan diteruskan sebagai nilai — pengembang secara eksplisit menulis kode untuk penerusan atau transformasi kesalahan. Menurut Kotlin Result Docs (2026), Result<T> di Kotlin tidak dimaksudkan untuk propagasi langsung melintasi batas fungsi — ia harus ditransformasi atau ditangani di setiap tingkat, yang membuat propagasi lebih sadar tetapi juga lebih bertele-tele.

Pemilihan pendekatan tergantung pada arsitektur aplikasi dan bahasa. Di Swift, propagasi otomatis melalui throws mendominasi, di Kotlin — campuran exception (untuk kesalahan tak terduga) dan wadah mirip Result (untuk kesalahan yang diharapkan). Penting untuk dipahami: propagasi bukanlah tujuan, melainkan kebutuhan. Arsitektur ideal meminimalkan kedalaman propagasi dengan menangani kesalahan pada tingkat serendah mungkin di mana terdapat cukup konteks untuk mengambil keputusan.

Propagasi melalui Throws di Swift

Di Swift, propagasi melalui throws terjadi secara otomatis: jika fungsi A dengan throws memanggil fungsi B dengan throws, dan A tidak menangani kesalahan B di do-catch, kesalahan secara otomatis diteruskan ke pihak pemanggil A. Ini menghilangkan kode boilerplate yang khas dari Java checked exceptions, di mana throws harus dideklarasikan di setiap metode rantai. Swift menggunakan prinsip „satu fungsi throws dalam rantai = seluruh rantai menjadi throws, jika tidak ditangani pada tingkat perantara”.

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 — penangan akhir
func onButtonTap() {
    let vm = UserViewModel()
    do {
        let user = try vm.loadUser(id: 42)
        updateUI(user)
    } catch {
        showError("Gagal memuat pengguna")
    }
}

Rantai propagasi: networkService.request -> fetchUser -> loadUser -> onButtonTap. Setiap fungsi perantara ditandai dengan throws dan tidak berisi do-catch — kesalahan secara otomatis diteruskan ke atas. ViewController onButtonTap adalah penangan akhir dengan do-catch. Jika ViewModel memutuskan untuk mentransformasi kesalahan (membungkusnya ke tipe lain), ia dapat menggunakan do-catch dan throw baru. Propagasi otomatis memperpendek kode: Repository tidak perlu tahu cara menangani kesalahan — itu adalah tanggung jawab ViewController, yang memiliki akses ke UI untuk menampilkan pesan kepada pengguna.

Propagasi melalui exception di Kotlin

Di Kotlin, propagasi melalui exception tidak memerlukan deklarasi throws di tanda tangan (semua exception unchecked). Exception secara otomatis naik melalui tumpukan hingga menemukan try-catch. Namun, ketiadaan throws di tanda tangan membuat propagasi implisit: pengembang tidak melihat dari tanda tangan fungsi bahwa ia dapat melempar exception. Ini adalah keuntungan (lebih sedikit boilerplate) dan kerugian (lebih mudah lupa menangani) sekaligus. Kotlin memecahkan masalah ini melalui konvensi dan pola arsitektur, bukan melalui bahasa.

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

Di Repository propagasi dengan transformasi: saat IOException (jaringan tidak tersedia) fungsi mencoba mengambil data dari cache di database. Jika cache kosong, ia melempar AppException — propagasi berlanjut dengan tipe kesalahan baru. ViewModel menangkap AppException dan menerjemahkannya ke UiState.Error — kesalahan tidak berjalan lebih jauh, propagasi selesai di tingkat lapisan UI. Kotlin Coroutines menambahkan kekhasan: exception di launch secara otomatis menyebar melalui CoroutineExceptionHandler, dan di async — hanya saat pemanggilan await(). Penting untuk mempertimbangkan ini saat merancang propagasi di coroutine — SupervisorJob mencegah pembatalan coroutine induk saat terjadi kesalahan di coroutine anak.

Propagasi melalui Tipe Result

Alternatif untuk exception — propagasi melalui wadah tipe yang meneruskan keberhasilan atau kesalahan sebagai nilai. Dalam pendekatan ini, fungsi tidak mengembalikan nilai, melainkan bungkusan: Result<T, E> di Swift, Result<T> di Kotlin, Either<L, R> di Dart (dari paket fpdart atau dartz). Kesalahan tidak membuka tumpukan — ia hanya berada di wadah, dan tingkat berikutnya memutuskan apa yang harus dilakukan dengannya. Ini membuat propagasi lebih eksplisit dan terkendali.

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> — wadah sederhana dengan bidang data dan error. Sealed class AppError mendefinisikan tipe kesalahan (Network, Auth). Fungsi fetchUser mengembalikan HttpResult, propagasi tidak memerlukan pembukaan tumpukan — pihak pemanggil cukup memeriksa isSuccess/isError. Pendekatan ini sangat berguna dalam Clean Architecture, di mana setiap lapisan (data, domain, presentation) dapat mentransformasi kesalahan: IOError -> DomainError -> UiError. Propagasi melalui wadah membuat transformasi ini eksplisit dan dapat diuji, tidak seperti exception di mana rantai transformasi tidak terlihat di tanda tangan fungsi.

Propagasi vs Penanganan: kapan meneruskan, kapan menangani

Salah satu keputusan kunci dalam merancang penanganan kesalahan adalah pilihan antara propagasi (meneruskan ke atas) dan penanganan (menangani di sini). Aturan pengambilan keputusan: tangani kesalahan pada tingkat di mana terdapat cukup konteks untuk tindakan yang bermakna. Jika Anda memiliki akses ke UI — tampilkan pesan kepada pengguna. Jika Anda memiliki akses ke cache — coba pulihkan. Jika tidak keduanya — propagasikan.

SkenarioTindakanAlasan
Kesalahan jaringan di RepositoryPropagasiRepository tidak tahu apakah pengguna ingin mengulangi permintaan
Kesalahan parsing di RepositoryTangani (kembalikan nilai default)Repository tahu formatnya, dapat mengembalikan nilai fallback
Timeout di ViewModelTangani (UiState.Error)ViewModel mengelola UiState, tahu cara menerjemahkan kesalahan
Kesalahan otorisasi di InterceptorTangani (refresh token)Interceptor memiliki akses ke token dan dapat memulihkan sesi
Kesalahan tidak dikenal di UseCasePropagasiUseCase tidak memiliki konteks UI — hanya logika bisnis

Aturan emas: propagasi minimum, penanganan maksimum di tingkat yang lebih rendah. Jika Repository dapat pulih dari cache — ia harus melakukannya, tanpa meneruskan kesalahan ke atas. Jika ViewModel dapat menampilkan Snackbar — biarkan ia menampilkannya, tanpa memerlukan kode tambahan dari ViewController. Setiap tingkat propagasi meningkatkan kopling dan mempersulit pengujian. Menurut Google Android Architecture Guide (2026), disarankan untuk meminimalkan propagasi melintasi batas lapisan dengan menggunakan sealed class UiState untuk mewakili semua kemungkinan keadaan (Loading, Success, Error) di tingkat ViewModel dan tidak meneruskan exception langsung ke lapisan UI.

Masalah dan antipola Propagasi Kesalahan

Propagasi yang tidak benar adalah sumber bug yang sulit ditemukan di aplikasi mobile. Mari kita lihat lima masalah utama yang dihadapi pengembang dan cara mengatasinya.

Kehilangan konteks kesalahan

Masalah paling umum: selama propagasi, exception ditangkap, dicatat, dan yang baru dilempar tanpa exception asli. Pengembang kehilangan StackTrace dan tidak dapat memahami di mana tepatnya kesalahan terjadi. Di Swift gunakan perangkaian kesalahan: throw MyError(context: originalError). Di Kotlin: throw AppException(cause = originalException). Di Dart: throw AppException(message, originalException). Jangan pernah membuat exception baru tanpa meneruskan penyebab/kesalahan yang mendasarinya.

Mengabaikan kesalahan (catch kosong)

catch (e: Exception) { /* tidak ada */ } — antipola yang menyebabkan aplikasi terus berjalan dalam keadaan yang salah. Jika Anda yakin kesalahan dapat diabaikan — tambahkan komentar dengan alasan. Di Swift untuk pengabaian opsional gunakan try? (kesalahan -> nil). Di Kotlin — Result<T>.onFailure { /* log */ }. Jangan matikan exception tanpa mencatatnya.

Kedalaman propagasi berlebihan

Jika kesalahan melewati 5+ tingkat tanpa penanganan, arsitektur perlu ditinjau kembali. Setiap tingkat propagasi adalah ketergantungan pada tanda tangan throws fungsi di bawahnya. Solusi: gunakan wadah Failure (sealed class Result { Success, Error }) di batas lapisan untuk membuat propagasi eksplisit dan terbatas. Semakin pendek rantai propagasi, semakin mudah menguji dan men-debug kode.

Propagasi di coroutine tanpa SupervisorJob

Di Kotlin Coroutines, exception di launch secara default membatalkan coroutine induk dan semua sibling (anak dari scope yang sama). Jika satu dari 10 tugas paralel gagal, 9 lainnya akan dibatalkan, yang sering tidak diinginkan. Gunakan SupervisorJob atau supervisorScope untuk isolasi kesalahan: kesalahan di satu child tidak membatalkan sibling. ViewModelScope menggunakan SupervisorJob secara default, yang melindungi dari masalah ini di Android.

Propagasi melalui callback tanpa penanganan

Di API berbasis callback, kesalahan sering diteruskan sebagai parameter callback. Jika callback tidak menangani kesalahan (atau menanganinya secara tidak benar), propagasi menjadi implisit dan mudah hilang. Solusi: migrasikan ke async/await (Swift) atau coroutine (Kotlin), di mana propagasi bekerja melalui mekanisme standar try-catch. Jika callback tidak terhindarkan — gunakan Either<Error, T> atau Result<T> untuk penanganan paksa kedua kasus.

Pertanyaan Umum

Apa perbedaan Propagasi Kesalahan dengan throw?

Throw adalah tindakan satu kali melempar exception. Propagasi Kesalahan adalah seluruh proses penerusan kesalahan melalui beberapa tingkat tumpukan, dari throw hingga catch. Propagasi mencakup throw, penerusan otomatis atau manual melalui fungsi perantara, dan penanganan akhir. Ini adalah konsep yang lebih luas yang menggambarkan siklus hidup kesalahan.

Bagaimana cara menguji Propagasi Kesalahan?

Gunakan objek mock yang melempar exception dalam skenario yang diberikan. Periksa bahwa fungsi dengan benar mempropagasi atau menangani kesalahan melalui assertThrows (Kotlin/JUnit) atau XCTAssertThrowsError (Swift/XCTest). Untuk propagasi berbasis Result, periksa isSuccess/isError dan nilai dalam kedua kasus.

Kapan propagasi melalui Result lebih baik daripada exception?

Propagasi Result lebih disukai untuk kesalahan yang diharapkan (data tidak valid, aturan bisnis) dalam satu batas arsitektur. Exception lebih baik untuk kesalahan tak terduga (kehilangan jaringan, kesalahan I/O) yang harus ditangani di tingkat tinggi. Hasil dengan kesalahan tidak menghentikan aliran eksekusi, exception menghentikannya.

Bagaimana cara mempropagasi kesalahan melalui coroutine Kotlin?

Di Kotlin Coroutines, exception di launch secara otomatis mempropagasi melalui CoroutineScope dengan pembatalan sibling. Gunakan supervisorScope atau SupervisorJob untuk isolasi: kesalahan di satu coroutine tidak membatalkan yang lain. Untuk async, kesalahan harus ditangani secara eksplisit melalui try-catch saat memanggil await(), jika tidak maka akan ditelan.

Tingkat mana yang harus menjadi penangan akhir kesalahan?

Penangan akhir yang ideal adalah lapisan UI (ViewController, Fragment/Composable). Hanya lapisan ini yang memiliki akses ke antarmuka pengguna dan dapat menampilkan pesan, Snackbar, atau dialog. Lapisan perantara (Repository, UseCase, ViewModel) mempropagasi kesalahan, mentransformasikannya jika perlu ke tipe domain yang lebih abstrak.

Kesimpulan

  • Propagasi Kesalahan — penerusan kesalahan ke atas melalui tumpukan dari tempat terjadinya ke penangan melalui exception atau wadah Result
  • Propagasi otomatis di Swift melalui throws tidak memerlukan kode di tingkat perantara — kesalahan naik sendiri
  • Propagasi manual di Kotlin melalui try-catch dan throw eksplisit di setiap tingkat membuat penanganan sadar tetapi bertele-tele
  • Tipe Result meneruskan kesalahan sebagai nilai tanpa membuka tumpukan, berguna untuk kesalahan yang diharapkan di Clean Architecture
  • Aturan penanganan: tangani di tingkat dengan konteks (UI), propagasi melalui tingkat tanpa konteks (domain, data)
  • Antipola: catch kosong, kehilangan penyebab saat transformasi, kedalaman propagasi berlebihan, mengabaikan SupervisorJob
  • Rancang kesalahan yang diketik (sealed class / enum) untuk setiap lapisan dan transformasikan saat melintasi batas lapisan

Kami akan mengembangkan aplikasi seluler turnkey

IT Sectr membuat aplikasi iOS dan Android untuk startup dan bisnis sejak 2017. Kami akan memberi saran dan mengusulkan solusi terbaik.

Diskusikan proyek

Baca juga