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 — 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.
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”.
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.
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.
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.
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.
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.
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.
| Skenario | Tindakan | Alasan |
|---|---|---|
| Kesalahan jaringan di Repository | Propagasi | Repository tidak tahu apakah pengguna ingin mengulangi permintaan |
| Kesalahan parsing di Repository | Tangani (kembalikan nilai default) | Repository tahu formatnya, dapat mengembalikan nilai fallback |
| Timeout di ViewModel | Tangani (UiState.Error) | ViewModel mengelola UiState, tahu cara menerjemahkan kesalahan |
| Kesalahan otorisasi di Interceptor | Tangani (refresh token) | Interceptor memiliki akses ke token dan dapat memulihkan sesi |
| Kesalahan tidak dikenal di UseCase | Propagasi | UseCase 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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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
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.
Baca juga