Java membagi exception menjadi dua kategori: checked yang wajib dideklarasikan di signature function, dan unchecked yang bebas dari aturan tersebut. Kotlin menghapus pembedaan ini — semua exception diperlakukan unchecked. Keputusan ini menyederhanakan kode, namun juga menggeser tanggung jawab disiplin dokumentasi ke pengembang. Pada chapter ini kita akan melihat perbedaan keduanya, demonstrasi Kotlin yang melempar IOException tanpa throws, serta kapan anotasi @Throws tetap berguna untuk interop dengan Java.

Pembedaan di Java #

Di Java, setiap exception yang mewarisi Throwable tapi bukan turunan RuntimeException atau Error bersifat checked. Compiler Java mengharuskan function yang melempar exception checked untuk mendeklarasikannya dengan keyword throws, dan setiap pemanggil wajib menangkap atau mendeklarasikannya kembali.

java Java: IOException adalah checked
// Di Java, signature wajib mencantumkan `throws IOException`
public String bacaFile(String path) throws IOException {
    FileInputStream stream = new FileInputStream(path);  // bisa melempar IOException
    return new String(stream.readAllBytes());
}

Tujuan awal checked exception adalah memaksa pemanggil sadar akan kegagalan yang mungkin terjadi. Dalam praktiknya, aturan ini sering kali menambah boilerplate: pemanggil yang tidak punya strategi pemulihan akhirnya menangkap dan mengabaikan, atau membungkus ke RuntimeException agar terbebas dari deklarasi.

Kotlin Menghapus Pembedaan #

Kotlin tidak memiliki konsep checked exception. Function Kotlin boleh melempar exception apa pun tanpa mendeklarasikannya di signature. Pemanggil bebas memilih: menangkap bila punya strategi, atau membiarkan naik bila tidak. Compiler Kotlin tidak menghasilkan error bila kita memanggil function Java yang melempar checked exception dan tidak menangkapnya.

kotlin Kotlin: IOException tidak perlu deklarasi
import java.io.FileInputStream
import java.io.IOException

fun bacaFileBesar(path: String): String {
    // FileInputStream konstruktor melempar FileNotFoundException (checked di Java).
    // Kotlin tidak memaksa deklarasi `throws` di sini.
    val stream = FileInputStream(path)
    return stream.use { it.readBytes().decodeToString() }
}

Kode di atas memanggil konstruktor FileInputStream yang melempar FileNotFoundException (checked di Java). Compiler Kotlin tidak mengeluh meskipun bacaFileBesar tidak mendeklarasikan throws IOException. Pemanggil bebas menangkap atau tidak.

Demonstrasi Lengkap #

Contoh berikut menunjukkan Kotlin yang melempar IOException, anotasi @Throws untuk interop Java, dan demonstrasi bahwa exception checked Java seperti ParseException dapat dilempar dan ditangkap tanpa deklarasi.

kotlin
// Demo checked vs unchecked: Kotlin tidak memaksa deklarasi throws; interop dengan Java.

import java.io.FileInputStream
import java.io.IOException

// Function Kotlin yang melempar IOException — tidak ada `throws` di signature.
fun bacaFileBesar(path: String): String {
    // FileInputStream konstruktor melempar FileNotFoundException (checked di Java).
    // Kotlin tidak memaksa kita mendeklarasikan `@Throws` di sini.
    val stream = FileInputStream(path)
    return stream.use {
        it.readBytes().decodeToString()
    }
}

// Bila ingin interop dengan Java yang baik, tambahkan @Throws secara eksplisit.
@Throws(IOException::class)
fun bacaFileUntukJava(path: String): String {
    val stream = FileInputStream(path)
    return stream.use { it.readBytes().decodeToString() }
}

fun tangkapIOException(path: String): String {
    return try {
        bacaFileBesar(path)
    } catch (e: IOException) {
        "[tangkap] ${e::class.simpleName}: ${e.message}"
    } catch (e: RuntimeException) {
        "[tangkap] ${e::class.simpleName}: ${e.message}"
    }
}

fun lemparDenganAtauTanpaThrows(): String {
    // Tidak ada perbedaan perilaku runtime antara function yang dianotasi @Throws
    // dan yang tidak. Perbedaannya hanya di signature bytecode yang dilihat Java.
    return "Perilaku runtime identik; @Throws hanya memengaruhi signature bytecode untuk Java."
}

fun bandingkanCheckedUnchecked(): String {
    val sb = StringBuilder()
    sb.appendLine("Kategori exception di JVM:")
    sb.appendLine("- Checked  (di Java): turunan Exception selain RuntimeException, wajib dideklarasikan.")
    sb.appendLine("- Unchecked (di Java): turunan RuntimeException atau Error, bebas dari deklarasi.")
    sb.appendLine("- Kotlin    : SEMUA exception diperlakukan unchecked (tidak ada `throws` wajib).")
    return sb.toString()
}

fun main() {
    println("=== Tangkap IOException dari bacaFileBesar ===")
    println(tangkapIOException("/file/tidak/ada.txt"))

    println()
    println("=== Catatan @Throws untuk interop Java ===")
    println(lemparDenganAtauTanpaThrows())

    println()
    println("=== Ringkasan kategori ===")
    println(bandingkanCheckedUnchecked())

    println()
    println("=== Demo lempar turunan Exception non-RuntimeException ===")
    // java.text.ParseException adalah checked di Java; di Kotlin kita tangkap bebas.
    try {
        throw java.text.ParseException("parse gagal di posisi 5", 5)
    } catch (e: java.text.ParseException) {
        println("tangkap ${e::class.simpleName}: ${e.message}, errorOffset=${e.errorOffset}")
    }
}

Output yang Dihasilkan:

text
=== Tangkap IOException dari bacaFileBesar ===
[tangkap] FileNotFoundException: /file/tidak/ada.txt (No such file or directory)

=== Catatan @Throws untuk interop Java ===
Perilaku runtime identik; @Throws hanya memengaruhi signature bytecode untuk Java.

=== Ringkasan kategori ===
Kategori exception di JVM:
- Checked  (di Java): turunan Exception selain RuntimeException, wajib dideklarasikan.
- Unchecked (di Java): turunan RuntimeException atau Error, bebas dari deklarasi.
- Kotlin    : SEMUA exception diperlakukan unchecked (tidak ada `throws` wajib).


=== Demo lempar turunan Exception non-RuntimeException ===
tangkap ParseException: parse gagal di posisi 5, errorOffset=5

Mari kita amati bagian-bagian penting dari kode di atas:

  1. bacaFileBesar: Tidak ada throws di signature Kotlin, meskipun konstruktor FileInputStream melempar FileNotFoundException (checked di Java). Pemanggil dapat menangkap IOException bila perlu, tetapi tidak dipaksa.
  2. bacaFileUntukJava: Diberi anotasi @Throws(IOException::class). Perilaku runtime identik dengan bacaFileBesar, namun bytecode yang dihasilkan memiliki signature throws IOException di file .class. Compiler Java yang memanggil function ini dari kode Java akan mengenali dan memperlakukannya sebagai checked.
  3. tangkapIOException: Membuktikan bahwa exception dari bacaFileBesar dapat ditangkap secara bebas. Bila path tidak ada, FileNotFoundException muncul dan ditangkap oleh cabang IOException (karena FileNotFoundException adalah subkelasnya).
  4. bandingkanCheckedUnchecked: Merangkum tiga kategori: checked di Java, unchecked di Java, dan Kotlin yang menyatukan keduanya sebagai unchecked.
  5. ParseException di main: java.text.ParseException adalah checked di Java, namun Kotlin mengizinkan kita melempar dan menangkap tanpa deklarasi. Field errorOffset dapat diakses langsung sebagai konteks tambahan.

Tabel Perbandingan #

AspekJavaKotlin
Turunan RuntimeExceptionUnchecked (bebas)Unchecked
Turunan Exception non-RuntimeChecked (wajib deklarasi)Unchecked (bebas)
ErrorUncheckedUnchecked
Keyword throws di signatureWajib untuk checkedTidak ada keyword throws
Anotasi @ThrowsTidak ada (pakai keyword throws)Opsional, untuk interop Java
Pemanggil wajib tangkap?Ya untuk checkedTidak, semua bebas

Trade-off Tanpa Checked Exception #

Menghapus checked exception memberi beberapa keuntungan: signature function lebih ringkas, lambda lebih mudah ditulis (tidak perlu membungkus checked exception), dan refactoring tidak tersekat oleh hierarki exception. Namun, trade-off ini menuntut disiplin yang lebih tinggi dari pengembang.

Tanpa deklarasi throws, pemanggil tidak punya cara otomatis untuk mengetahui exception apa yang mungkin dilempar. Solusinya adalah dokumentasi KDoc eksplisit:

kotlin Dokumentasi exception di KDoc
/**
 * Membaca konfigurasi dari path yang diberikan.
 *
 * @return Isi file konfigurasi sebagai string.
 * @throws IOException bila file tidak dapat dibaca atau tidak ditemukan.
 * @throws IllegalStateException bila format konfigurasi tidak valid.
 */
fun bacaKonfigurasi(path: String): String { ... }

Dengan KDoc yang lengkap, pemanggil tahu kapan harus menangkap tanpa mengandalkan compiler.

Kapan @Throws Diperlukan #

Anotasi @Throws hanya mempengaruhi signature bytecode yang dilihat Java. Kotlin murni tidak peduli. Gunakan @Throws bila kode Kotlin dipanggil dari Java dan Anda ingin compiler Java memperlakukan exception sebagai checked, atau bila library publik dipakai lintas bahasa.

Referensi Lanjutan #

Penutup #

Kotlin menyatukan checked dan unchecked menjadi satu kategori: unchecked. Signature function lebih ringkas dan refactoring lebih luwes, namun disiplin dokumentasi menjadi pengganti peran compiler. Untuk interop Java, anotasi @Throws tetap berguna agar konsumen Java memperlakukan exception sesuai konvensinya. Berikutnya, kita akan melihat alternatif lain untuk merepresentasikan kegagalan: tipe Result<T> yang memuat nilai sukses atau kegagalan secara eksplisit tanpa melempar exception.