Java membagi exception menjadi checked (wajib deklarasi throws atau tangkap) dan unchecked (bebas). Kotlin tidak memiliki konsep checked exception — semua exception diperlakukan sebagai unchecked. Saat kode Kotlin memanggil method Java yang melempar checked exception, Kotlin tidak memaksa kita mendeklarasikan throws atau membungkus dalam try-catch. Namun exception tersebut tetap dapat dilempar saat runtime, sehingga try-catch tetap berfungsi normal. Bab ini membahas perilaku ini, peran @Throws untuk konsumsi Java, dan pola multi-catch Kotlin.

Perilaku Default: Checked di Java, Unchecked di Kotlin #

Saat method Java seperti FileInputStream(String) yang dideklarasikan throws FileNotFoundException dipanggil dari Kotlin, kompilator Kotlin tidak memaksa penanganan. Kode berikut valid tanpa throws atau try-catch:

kotlin Pola: pemanggilan tanpa deklarasi throws
fun bacaBaris(path: String): String {
    FileInputStream(path).use { it.read().toChar().toString() }
}

Di Java, kode di atas tidak akan dikompilasi tanpa throws IOException atau try-catch. Kotlin mengabaikan aturan checked exception karena dianggap terlalu kaku dan mendorong boilerplate yang tidak menambah keamanan nyata.

Sumber ExceptionJavaKotlin
IOException (checked di Java)Wajib throws/catchTidak wajib, bebas
ParseException (checked di Java)Wajib throws/catchTidak wajib, bebas
RuntimeException (unchecked)BebasBebas
OutOfMemoryError (Error)BebasBebas

Meski tidak wajib, exception tetap dilempar saat runtime. Kode Kotlin dapat menangkapnya dengan try-catch kapan pun diperlukan.

Contoh Lengkap: Tangkap Exception Java dari Kotlin #

Kode berikut memanggil method Java berisiko (FileInputStream, SimpleDateFormat.parse) tanpa deklarasi throws, lalu menangkap exception-nya secara terstruktur:

kotlin
import java.io.FileInputStream
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date

// Demonstrasi checked exceptions interoperability:
// - Java checked exceptions (IOException, ParseException) TIDAK diwajibkan
//   deklarasi throws di Kotlin.
// - Kotlin TETAP dapat menangkapnya dengan try-catch.
// - @Throws untuk kompatibilitas Java: bila kode Java akan memanggil Kotlin
//   dan ingin melihat checked exception pada signature-nya.

// 1. Fungsi Kotlin yang membaca berkas tanpa deklarasi 'throws IOException'
fun bacaBarisPertama(path: String): String {
    FileInputStream(path).use { fis ->
        val byte = fis.read()
        return if (byte == -1) "" else byte.toChar().toString()
    }
}

// 2. Fungsi Kotlin yang memparse tanggal tanpa deklarasi 'throws ParseException'
fun parseTanggalSimple(tanggalStr: String): Date {
    val fmt = SimpleDateFormat("yyyy-MM-dd")
    return fmt.parse(tanggalStr)
}

// 3. @Throws untuk konsumsi Java: signature Kotlin memiliki throws IOException
@Throws(IOException::class)
fun bacaBarisPertamaUntukJava(path: String): String {
    FileInputStream(path).use { fis ->
        val byte = fis.read()
        return if (byte == -1) "" else byte.toChar().toString()
    }
}

fun main() {
    // 1. Tangkap IOException dari FileInputStream (checked di Java,Unchecked di Kotlin)
    println("=== Tangkap IOException ===")
    try {
        val isi = bacaBarisPertama("/tmp/berkas_tidak_ada.txt")
        println("isi: $isi")
    } catch (e: IOException) {
        println("Tangkap IOException: ${e.javaClass.simpleName}")
        println("  pesan: ${e.message}")
    }

    // 2. Tangkap ParseException dari SimpleDateFormat
    println()
    println("=== Tangkap ParseException ===")
    val inputBerisiko = listOf("2026-01-15", "bukan-tanggal", "2026/01/15")
    for (input in inputBerisiko) {
        try {
            val tanggal = parseTanggalSimple(input)
            println("input: ${input.padEnd(20)} -> sukses: $tanggal")
        } catch (e: java.text.ParseException) {
            println("input: ${input.padEnd(20)} -> ParseException: ${e.message}")
        }
    }

    // 3. Exception bertele (chained) tetap dapat ditangkap
    println()
    println("=== Tangkap ClassCastException ===")
    try {
        val apa: Any = "ini string"
        val angka: Int = apa as Int   // melempar ClassCastException
    } catch (e: ClassCastException) {
        println("Tangkap: ${e.javaClass.simpleName}")
        println("  pesan: ${e.message}")
    }

    // 4. Multiple exception dalam satu catch via '|'
    println()
    println("=== Multi-catch ===")
    val data = listOf("2026-01-15", "tidak-valid", "/tmp/file_lagi_tidak_ada")
    for (d in data) {
        try {
            if (d.contains("-") && d.length == 10) {
                val t = parseTanggalSimple(d)
                println("$d -> sukses parse: $t")
            } else if (d.startsWith("/")) {
                val isi = bacaBarisPertama(d)
                println("$d -> sukses baca: $isi")
            } else {
                println("$d -> format tidak dikenali")
            }
        } catch (e: IOException) {
            println("$d -> IOException: ${e.message}")
        } catch (e: java.text.ParseException) {
            println("$d -> ParseException: ${e.message}")
        }
    }
}

Output yang Dihasilkan:

text
=== Tangkap IOException ===
Tangkap IOException: FileNotFoundException
  pesan: /tmp/berkas_tidak_ada.txt (No such file or directory)

=== Tangkap ParseException ===
input: 2026-01-15           -> sukses: Thu Jan 15 00:00:00 UTC 2026
input: bukan-tanggal        -> ParseException: Unparseable date: "bukan-tanggal"
input: 2026/01/15           -> ParseException: Unparseable date: "2026/01/15"

=== Tangkap ClassCastException ===
Tangkap: ClassCastException
  pesan: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')

=== Multi-catch ===
2026-01-15 -> sukses parse: Thu Jan 15 00:00:00 UTC 2026
tidak-valid -> format tidak dikenali
/tmp/file_lagi_tidak_ada -> IOException: /tmp/file_lagi_tidak_ada (No such file or directory)

Mari kita telaah baris-baris kode di atas:

  1. Baris 14 (fun bacaBarisPertama(path: String): String): fungsi Kotlin memanggil FileInputStream(path) yang di Java wajib dideklarasikan throws IOException. Kotlin tidak memaksa deklarasi tersebut, sehingga signature Kotlin menjadi bersih. Bila path tidak ada, FileNotFoundException (subclass IOException) tetap dilempar saat runtime.
  2. Baris 22 (fun parseTanggalSimple(tanggalStr: String): Date): SimpleDateFormat.parse(String) di Java dideklarasikan throws ParseException. Kotlin mengabaikan aturan ini, sehingga signature Kotlin hanya menampilkan tipe kembalian Date tanpa klausa throws. Tanggung jawab penanganan dialihkan ke pemanggil.
  3. Baris 30 (@Throws(IOException::class) fun bacaBarisPertamaUntukJava(...)): anotasi @Throws menambah klausa throws pada bytecode Java. Sisi Java yang memanggil method ini akan melihat String bacaBarisPertamaUntukJava(String) throws IOException dan kompilator Java memaksa penanganan. Tanpa @Throws, sisi Java tidak tahu bahwa method dapat melempar checked exception.
  4. Baris 39-44 (tangkap IOException): walaupun tidak wajib, try-catch tetap berfungsi. e.javaClass.simpleName menghasilkan FileNotFoundException — subclass dari IOException yang ditangkap di catch. Pesan error berasal dari sistem operasi (linux).
  5. Baris 47-56 (tangkap ParseException): SimpleDateFormat.parse melempar ParseException saat format tidak cocok. "2026-01-15" cocok pattern "yyyy-MM-dd" dan berhasil. "bukan-tanggal" dan "2026/01/15" gagal parse dengan pesan jelas.
  6. Baris 59-65 (tangkap ClassCastException): cast apa as Int gagal karena apa adalah String. ClassCastException adalah RuntimeException (unchecked) di kedua bahasa, sehingga tangkapannya tidak ada hubungan dengan aturan checked.
  7. Baris 68-87 (multi-catch): dua catch terpisah menangani IOException dan ParseException dengan pesan berbeda. Kotlin juga mendukung sintaks catch (e: IOException | ParseException) (multi-catch single block), namun pemisahan dalam dua blok memberi pesan error yang lebih spesifik.

Kapan Memakai @Throws #

Anotasi @Throws hanya diperlukan bila kode Java akan memanggil method Kotlin dan kita ingin kompilator Java memaksa penanganan. Skenario tipikal:

  • Library Kotlin yang dikonsumsi aplikasi Java (Spring Boot backend dengan kode controller Kotlin).
  • Method Kotlin yang membungkus operasi I/O atau parsing berisiko.
  • Method callback yang dipanggil framework Java (mis. Callable, Comparator).
kotlin Pola: library Kotlin untuk konsumsi Java
class LayananRepo {
    @Throws(IOException::class)
    fun baca(path: String): String { ... }

    @Throws(IOException::class, JsonSyntaxException::class)
    fun bacaJson(path: String): Map<String, Any> { ... }
}

Tanpa @Throws, kode Java yang memanggil method ini akan menerima exception tanpa peringatan kompilator — bug tersekat yang sulit dideteksi.

Referensi Lanjutan #

Selanjutnya #

Bab berikutnya membahas static interop — bagaimana companion object Kotlin memetakan ke static method Java, dan bagaimana @JvmStatic serta @JvmField menghasilkan static sejati yang dapat dipanggil langsung dari Java tanpa perantara Companion.