Lewati ke isi

Exception Handling in Java

Pada tutorial kali ini, kita akan belajar materi basic dari exception handling pada Java. Jadi, bersiaplah...


1 | Aturan Pertama

1.1 | Apa itu Exception handling?

Untuk lebih memahami exception (pengecualian) dan exception handling (penanganan pengecualian), kita perlu membuat sebuah analogi nyata. Bayangkan kita melakukan sebuah pemesanan produk secara online, tetapi dijalan, rute yang biasa dilalui terdapat masalah . Perusahaan pengiriman yang baik akan mengatasi masalah ini dengan cara mencari atau mengalihkan rute pengiriman, sehingga produk akan tetap sampai tepat pada waktunya.

Seperti halnya pada Java, kode bisa mengalami error ketika dieksekusi dan dijalankan. Exception handling yang baik harus bisa menangani error dan tetap membuat program berjalan dengan baik, memberikan penggunanya pengalaman yang memuaskan.

1.2 | Kenapa Menggunakanya?

Kita bisanya menulis kode pada lingkungan yang baik, file di sistem kita menyediakan semua yang kita cari dan butuhkan, dan jaringan mungkin selalu tersedia, dan JVM selalu memiliki cukup memory yang tersedia. Terkadang, kita menyebutnya dengan "Happy path", atau jalur yang menyenangkan.

Tapi pada kode skala produksi, file sistem bisa mengalami corrupt, jaringan mungkin down, dan JVM kehabisan memory untuk bekerja. Kode kita harus dirancang untuk mampu menghadapi situasi dari "Unhappy path" atau jalan yang tidak menyenangkan ini.

Kita harus bisa menangani kondisi seperti ini karena bisa mengganggu kelancaran dan alur kerja program, dan berdampak negatif pada kinerja aplikasi.

public static List<Player> getPlayers() throws IOException {
    Path path = Paths.get("players.dat");
    List<String> players = Files.readAllLines(path);

    return players.stream()
      .map(Player::new)
      .collect(Collectors.toList());
}

Kode diatas tidak memiliki penangan IOException, dan langsung mengirimkannya ke call stack secara langsung. Pada lingkungan yang baik, kode tersebut mungkin bekerja.

Tapi, apa jadinya jika selama produksi, file player.dat ternyata hilang?

Exception in thread "main" java.nio.file.NoSuchFileException: players.dat <-- players.dat file doesn't exist
    at sun.nio.fs.WindowsException.translateToIOException(Unknown Source)
    at sun.nio.fs.WindowsException.rethrowAsIOException(Unknown Source)
    // ... more stack trace
    at java.nio.file.Files.readAllLines(Unknown Source)
    at java.nio.file.Files.readAllLines(Unknown Source)
    at Exceptions.getPlayers(Exceptions.java:12) <-- Exception arises in getPlayers() method, on line 12
    at Exceptions.main(Exceptions.java:19) <-- getPlayers() is called by main(), on line 19

Tanpa exception handling, kode yang seharusnya bekerja dengan baik akan berhenti bekerja sama sekali. Kode yang bagus seharusnya bisa mengatasi masalah ini, kita sebagai penulis kode harus memiliki rencana jika terdapat sesuatu yang salah.

Catat juga, satu keuntungan dari adanya exception handling, adalah kita bisa membuat stack trace. Dengan stack trace, kita bisa mengetahui bagian mana dari kode yang salah, tanpa perlu menggunakan debugger.


2 | Hierarki Exception

Pada dasarnya, exception hanyalah objek Java, dengan semua exception mewarisi dari Throwable:

              ---> Throwable <--- 
              |    (checked)     |
              |                  |
              |                  |
      ---> Exception           Error
      |    (checked)        (unchecked)
      |
RuntimeException
  (unchecked)

Ada tiga kategori utama dari kondisi exception:

  1. Checked exceptions
  2. Unchecked exceptions / Runtime exceptions
  3. Errors

Runtime exception dan unchecked exception merujuk pada hal yang sama, sehingga seringkali bisa digunakan secara bergantian.

2.1 | Checked Exception (Pengecualian Terperiksa)

Checked exception adalah jenis pengecualian yang wajib ditangani oleh programmer, karena kompiler Java memeriksanya saat proses kompilasi. Artinya, kita harus menangani pengecualian tersebut secara langsung (menggunakan try-catch) atau menyatakannya akan dilempar ke luar metode dengan kata kunci throws.

Menurut dokumentasi Oracle, checked exception digunakan ketika pemanggil metode masih mungkin melakukan pemulihan (recovery) dari kesalahan yang terjadi.

Contoh checked exception yang umum adalah: - IOException - ServletException

2.3 | Unchecked Exception (Pengecualian Tidak Terperiksa)

Unchecked exception adalah jenis pengecualian yang tidak diwajibkan oleh kompiler Java untuk ditangani.

Secara sederhana, jika kita membuat kelas exception yang mewarisi RuntimeException, maka exception tersebut termasuk unchecked; sedangkan jika tidak, maka ia tergolong checked.

Meskipun tampak lebih praktis, dokumentasi Oracle menegaskan bahwa kedua jenis exception memiliki tujuan yang berbeda — checked digunakan untuk kesalahan situasional yang dapat dipulihkan, sedangkan unchecked digunakan untuk kesalahan akibat penggunaan kode yang salah (usage error).

Beberapa contoh unchecked exception:

  • NullPointerException
  • IllegalArgumentException
  • SecurityException

2.3 | Error (Kesalahan Fatal)

Error mewakili kondisi serius dan umumnya tidak dapat dipulihkan, seperti ketidakcocokan pustaka, rekursi tak berujung, atau kebocoran memori.

Meskipun kelas Error tidak mewarisi RuntimeException, ia tetap termasuk dalam kategori unchecked, artinya kompiler tidak memaksa kita untuk menanganinya.

Dalam praktiknya, menangani, membuat instance, atau menurunkan kelas Error adalah hal yang tidak lazim. Biasanya, error dibiarkan mengalir hingga ke level tertinggi agar program berhenti, karena ini menandakan adanya masalah serius pada sistem.

Contoh error yang umum:

  • StackOverflowError
  • OutOfMemoryError

3 | Handling Exceptions

Dalam Java API, terdapat banyak bagian kode yang berpotensi menimbulkan kesalahan, dan beberapa di antaranya ditandai dengan exception, baik di deklarasi metode (signature) maupun di dokumentasi Javadoc, seperti berikut:

/**
 * @exception FileNotFoundException ...
 */
public Scanner(String fileName) throws FileNotFoundException {
   // ...
}

Seperti yang sudah dijelaskan sebelumnya, ketika kita memanggil metode yang “berisiko” semacam ini, kita wajib menangani checked exception, sedangkan unchecked exception boleh ditangani atau tidak — tergantung kebutuhan.

Java menyediakan beberapa cara untuk menangani exception tersebut.

3.1 | throws

Cara paling sederhana untuk "menangani" sebuah exception adalah dengan melemparkannya kembali menggunakan kata kunci throws:

public int getPlayerScore(String playerFile)
  throws FileNotFoundException {

    Scanner contents = new Scanner(new File(playerFile));
    return Integer.parseInt(contents.nextLine());
}

Karena FileNotFoundException merupakan checked exception, inilah cara paling sederhana untuk memenuhi aturan kompiler. Namun, akibatnya, setiap kode yang memanggil metode tersebut juga harus menangani exception itu!

Sementara itu, parseInt dapat melempar NumberFormatException, tetapi karena jenisnya unchecked, kita tidak diwajibkan untuk menanganinya.

3.2 | try-catch

Jika kita ingin menangani exception secara langsung, kita dapat menggunakan blok try-catch. Penanganannya bisa dilakukan dengan dua cara:

  1. Melempar kembali exception dalam bentuk lain:
public int getPlayerScore(String playerFile) {
    try {
        Scanner contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException noFile) {
        throw new IllegalArgumentException("File not found");
    }
}
  1. Melakukan langkah pemulihan (recovery):
public int getPlayerScore(String playerFile) {
    try {
        Scanner contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException noFile) {
        logger.warn("File not found, resetting score.");
        return 0;
    }
}

3.3 | finally

Ada kalanya kita memiliki kode yang harus dijalankan terlepas dari apakah terjadi exception atau tidak, dan di sinilah kata kunci finally digunakan.

Dalam contoh sebelumnya, sebenarnya ada bug tersembunyi: secara default, Java tidak otomatis mengembalikan file handle ke sistem operasi. Karena itu, baik file berhasil dibaca atau tidak, kita harus memastikan proses pembersihan (cleanup) dilakukan dengan benar.

Contoh paling sederhana:

public int getPlayerScore(String playerFile)
  throws FileNotFoundException {
    Scanner contents = null;
    try {
        contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } finally {
        if (contents != null) {
            contents.close();
        }
    }
}

Pada contoh di atas, blok finally menunjukkan kode yang akan selalu dijalankan, apa pun yang terjadi saat mencoba membaca file. Bahkan jika FileNotFoundException dilempar ke luar metode, isi dari blok finally tetap akan dijalankan terlebih dahulu.

Kita juga bisa menangani exception sekaligus memastikan sumber daya ditutup dengan aman:

public int getPlayerScore(String playerFile) {
    Scanner contents;
    try {
        contents = new Scanner(new File(playerFile));
        return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException noFile) {
        logger.warn("File not found, resetting score.");
        return 0; 
    } finally {
        try {
            if (contents != null) {
                contents.close();
            }
        } catch (IOException io) {
            logger.error("Couldn't close the reader!", io);
        }
    }
}

Karena metode close() juga termasuk “berisiko”, kita harus menangani exception yang mungkin muncul darinya.

Meskipun terlihat rumit, setiap bagian dari kode ini memiliki peran penting untuk menangani setiap kemungkinan kesalahan dengan tepat.

3.4 | try-with-resources

Sejak Java 7, penulisan kode seperti contoh sebelumnya bisa dibuat jauh lebih sederhana saat bekerja dengan objek yang mengimplementasikan AutoCloseable.

Contohnya:

public int getPlayerScore(String playerFile) {
    try (Scanner contents = new Scanner(new File(playerFile))) {
        return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException e) {
        logger.warn("File not found, resetting score.");
        return 0;
    }
}

Dengan menempatkan objek yang bersifat AutoCloseable di dalam deklarasi try, kita tidak perlu lagi menutup resource secara manual — Java akan otomatis menutupnya setelah blok try selesai dieksekusi, baik berhasil maupun gagal.

Kita tetap bisa menambahkan blok finally jika masih ada proses pembersihan lain yang perlu dilakukan.

Untuk pembahasan lebih lengkap, bisa melihat artikel khusus yang membahas tentang try-with-resources.

3.5 | Multiple catch blocks

Kadang, sebuah blok kode bisa melempar lebih dari satu jenis exception, dan kita bisa menanganinya dengan beberapa blok catch yang berbeda:

public int getPlayerScore(String playerFile) {
    try (Scanner contents = new Scanner(new File(playerFile))) {
        return Integer.parseInt(contents.nextLine());
    } catch (IOException e) {
        logger.warn("Player file wouldn't load!", e);
        return 0;
    } catch (NumberFormatException e) {
        logger.warn("Player file was corrupted!", e);
        return 0;
    }
}

Beberapa blok catch memungkinkan kita menangani setiap exception secara terpisah sesuai kebutuhan.

Perhatikan bahwa pada contoh di atas, FileNotFoundException tidak ditangani secara eksplisit, karena kelas tersebut merupakan turunan dari IOException. Dengan menangani IOException, Java secara otomatis menganggap semua subclass-nya juga sudah ditangani.

Namun, jika kita ingin memperlakukan FileNotFoundException secara berbeda dari IOException yang lebih umum, kita bisa menulisnya seperti ini:

public int getPlayerScore(String playerFile) {
    try (Scanner contents = new Scanner(new File(playerFile))) {
        return Integer.parseInt(contents.nextLine());
    } catch (FileNotFoundException e) {
        logger.warn("Player file not found!", e);
        return 0;
    } catch (IOException e) {
        logger.warn("Player file wouldn't load!", e);
        return 0;
    } catch (NumberFormatException e) {
        logger.warn("Player file was corrupted!", e);
        return 0;
    }
}

Java memungkinkan kita menangani subclass exception secara terpisah, tetapi pastikan urutannya benar — exception yang lebih spesifik harus ditangani terlebih dahulu sebelum yang lebih umum.

3.6 | Union catch blocks

Jika kita tahu bahwa beberapa exception akan ditangani dengan cara yang sama, sejak Java 7 kita bisa menggunakan satu blok catch untuk menangkap lebih dari satu jenis exception sekaligus:

public int getPlayerScore(String playerFile) {
    try (Scanner contents = new Scanner(new File(playerFile))) {
        return Integer.parseInt(contents.nextLine());
    } catch (IOException | NumberFormatException e) {
        logger.warn("Failed to load score!", e);
        return 0;
    }
}

Dengan sintaks seperti ini, Java akan menangkap salah satu dari exception yang disebutkan di dalam tanda pemisah |, lalu menanganinya menggunakan blok kode yang sama.

Fitur ini membantu membuat kode lebih ringkas dan mudah dibaca ketika penanganan untuk beberapa jenis exception identik.


4 | Throwing Exceptions

Jika kita tidak ingin menangani exception secara langsung, atau ingin membuat exception sendiri agar bisa ditangani oleh kode lain, maka kita perlu memahami penggunaan kata kunci throw.

Misalnya, kita membuat sendiri sebuah checked exception bernama TimeoutException:

public class TimeoutException extends Exception {
    public TimeoutException(String message) {
        super(message);
    }
}

Lalu kita memiliki sebuah metode yang mungkin membutuhkan waktu lama untuk diselesaikan:

public List<Player> loadAllPlayers(String playersFile) {
    // ... potentially long operation
}

4.1 | Throwing a checked exception

Sama seperti pernyataan return, kita bisa menggunakan throw kapan saja di dalam metode.

Biasanya, kita melempar exception untuk menandakan bahwa terjadi kesalahan atau kondisi yang tidak normal:

public List<Player> loadAllPlayers(String playersFile) throws TimeoutException {
    while (!tooLong) {
        // ... potentially long operation
    }
    throw new TimeoutException("This operation took too long");
}

Karena TimeoutException merupakan checked exception, kita juga harus menambahkan kata kunci throws pada deklarasi metode agar pemanggil metode mengetahui bahwa exception tersebut perlu ditangani.

4.2 | Throwing an unchecked exception

Jika kita ingin melakukan sesuatu seperti memvalidasi input, kita bisa menggunakan unchecked exception:

public List<Player> loadAllPlayers(String playersFile) throws TimeoutException {
    if (!isFilenameValid(playersFile)) {
        throw new IllegalArgumentException("Filename isn't valid!");
    }

    // ...
}

Karena IllegalArgumentException termasuk unchecked exception, kita tidak wajib menandai metode dengan throws, meskipun tetap boleh dilakukan jika ingin.

Beberapa developer tetap menuliskannya sebagai bentuk dokumentasi agar lebih jelas bahwa metode tersebut dapat melempar exception tertentu.

4.4 | Wrapping and rethrowing

Kita juga bisa memilih untuk melempar kembali exception yang sudah kita tangkap:

public List<Player> loadAllPlayers(String playersFile) 
  throws IOException {
    try { 
        // ...
    } catch (IOException io) {      
        throw io;
    }
}

Atau, kita bisa membungkusnya dalam exception baru sebelum melemparkannya kembali:

public List<Player> loadAllPlayers(String playersFile) 
  throws PlayerLoadException {
    try { 
        // ...
    } catch (IOException io) {      
        throw new PlayerLoadException(io);
    }
}

Pendekatan ini berguna ketika kita ingin menyatukan berbagai jenis exception menjadi satu tipe yang lebih umum, sehingga penanganannya menjadi lebih sederhana di level yang lebih tinggi.

4.5 | Rethrowing Throwable atau Exception

Sekarang masuk ke kasus khusus.

Jika di dalam suatu blok kode hanya ada kemungkinan munculnya unchecked exception, maka kita bisa menangkap dan melempar kembali (rethrow) Throwable atau Exception tanpa perlu menambahkannya ke deklarasi metode dengan throws:

public List<Player> loadAllPlayers(String playersFile) {
    try {
        throw new NullPointerException();
    } catch (Throwable t) {
        throw t;
    }
}

Meskipun tampak sederhana, kode di atas tidak dapat melempar checked exception, sehingga walaupun kita menulis throw t, kompiler tidak mengharuskan adanya klausa throws pada deklarasi metode.

Teknik ini sering digunakan dalam proxy class atau metode dinamis, di mana kita perlu menangani dan melempar ulang berbagai jenis exception tanpa mendefinisikan semuanya satu per satu.

4.5 | Inheritance

Ketika kita menandai suatu metode dengan kata kunci throws, hal itu akan memengaruhi bagaimana subclass boleh melakukan override terhadap metode tersebut.

Jika metode di kelas induk melempar checked exception:

public class Exceptions {
    public List<Player> loadAllPlayers(String playersFile) 
      throws TimeoutException {
        // ...
    }
}

Maka subclass masih boleh membuat versi override dengan signature yang lebih aman (tidak melempar checked exception apa pun):

public class FewerExceptions extends Exceptions {   
    @Override
    public List<Player> loadAllPlayers(String playersFile) {
        // overridden
    }
}

Namun subclass tidak boleh membuat versi override yang lebih berisiko, yaitu yang menambahkan checked exception baru:

public class MoreExceptions extends Exceptions {        
    @Override
    public List<Player> loadAllPlayers(String playersFile) throws MyCheckedException {
        // overridden
    }
}

Alasannya adalah karena kontrak metode ditentukan pada waktu kompilasi berdasarkan tipe referensi, bukan tipe objek sebenarnya.

Contohnya:

Exceptions exceptions = new MoreExceptions();
exceptions.loadAllPlayers("file");

Kompiler hanya akan mengharuskan kita menangani TimeoutException, padahal implementasi di subclass MoreExceptions justru melempar exception lain (MyCheckedException).

Kesimpulannya: subclass boleh melempar lebih sedikit checked exception daripada superclass-nya, tapi tidak boleh lebih banyak.


5 | Anti-Patterns

5.1 | Swallowing exceptions

Ada satu cara lain untuk membuat kode kita tetap lolos dari pemeriksaan kompiler, yaitu dengan menangkap dan mengabaikan exception sepenuhnya:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (Exception e) {} // <== catch and swallow
    return 0;
}

Teknik ini disebut swallowing exception — artinya kita menangkap exception tapi tidak melakukan apa pun terhadapnya. Biasanya ini bukan praktik yang baik, karena masalahnya tidak diselesaikan, dan kode lain pun tidak bisa tahu bahwa ada kesalahan yang terjadi.

Ada kalanya memang kita tahu bahwa exception tersebut tidak mungkin terjadi, misalnya karena kondisi yang sudah terjamin oleh logika program. Dalam kasus seperti itu, sebaiknya tetap beri komentar yang menjelaskan bahwa exception tersebut sengaja diabaikan:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (IOException e) {
        // this will never happen
    }
}

Cara lain untuk “menelan” exception adalah dengan hanya mencetaknya ke error stream:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (Exception e) {
        e.printStackTrace();
    }
    return 0;
}

Pendekatan ini sedikit lebih baik, karena setidaknya kita menulis pesan kesalahan untuk diagnosis di kemudian hari. Namun, cara yang lebih disarankan adalah dengan menggunakan logger:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (IOException e) {
        logger.error("Couldn't load the score", e);
        return 0;
    }
}

Menangani exception dengan cara ini memang praktis, tapi kita perlu memastikan bahwa tidak ada informasi penting yang hilang, terutama jika pemanggil metode membutuhkan informasi itu untuk memperbaiki masalah.

Kita juga bisa tanpa sengaja menelan exception dengan tidak menyertakannya sebagai penyebab ketika melempar exception baru:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (IOException e) {
        throw new PlayerScoreException();
    }
}

Sekilas terlihat benar, karena kita melempar exception baru untuk memberi tahu bahwa ada kesalahan. Namun, kita kehilangan konteks asli (IOException) yang bisa membantu melacak akar masalahnya.

Solusi yang benar adalah menyertakan exception asli sebagai penyebab:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (IOException e) {
        throw new PlayerScoreException(e);
    }
}

Perbedaan kecil ini sangat penting — dengan menyertakan IOException sebagai penyebab, kita tetap mempertahankan jejak asal kesalahan sehingga lebih mudah untuk melakukan debug atau logging di kemudian hari.

5.2 | Using return in a finally block

Cara lain yang tanpa sadar bisa menelan exception adalah dengan menggunakan return di dalam blok finally. Ini berbahaya karena ketika finally mengembalikan nilai secara tiba-tiba, JVM akan mengabaikan exception yang dilempar dari blok try.

Contoh:

public int getPlayerScore(String playerFile) {
    int score = 0;
    try {
        throw new IOException();
    } finally {
        return score; // <== IOException diabaikan
    }
}

Dalam kasus di atas, IOException tidak akan pernah terlihat oleh pemanggil metode, karena perintah return di dalam finally menimpa aliran eksekusi sebelumnya.

Menurut Java Language Specification:

Jika eksekusi blok try berakhir secara tiba-tiba karena suatu alasan R (misalnya karena exception), maka blok finally akan dijalankan terlebih dahulu.

  • Jika blok finally berakhir secara normal, maka eksekusi try juga berakhir secara tiba-tiba karena alasan R.
  • Namun, jika blok finally juga berakhir secara tiba-tiba karena alasan lain S (misalnya return, throw, atau break), maka alasan R akan dibuang, dan try akan berakhir karena alasan S.

Artinya: jika kamu melakukan return, throw baru, atau keluar paksa dari finally, maka exception yang seharusnya muncul akan hilang total. Itu sebabnya, menempatkan return di dalam finally adalah praktik yang sebaiknya dihindari sepenuhnya.

5.3 | Menggunakan throw di dalam finally block

Mirip seperti penggunaan return di dalam blok finally, melempar exception (throw) di sana juga menggantikan exception sebelumnya yang mungkin muncul dari blok try atau catch.

Akibatnya, exception asli yang seharusnya muncul akan hilang, dan yang tersisa hanya exception dari blok finally.

Contoh:

public int getPlayerScore(String playerFile) {
    try {
        // ...
    } catch (IOException io) {
        throw new IllegalStateException(io); // <== tertimpa oleh finally
    } finally {
        throw new OtherException();
    }
}

Pada kode di atas, IllegalStateException yang dilempar dari blok catch akan terhapus, karena blok finally juga melempar OtherException. Akhirnya, hanya Other_exception_ yang terlihat keluar dari metode ini.

Kesimpulannya: Jangan pernah melempar exception baru dari dalam finally kecuali benar-benar yakin. Jika perlu menangani kondisi khusus di finally, sebaiknya lakukan dengan hati-hati—misalnya log error atau bersihkan sumber daya—tanpa mengacaukan exception utama yang sedang diproses.

5.4 | Menggunakan throw sebagai goto

Beberapa orang tergoda untuk menggunakan throw seolah-olah itu adalah pernyataan goto:

public void doSomething() {
    try {
        // sekumpulan kode pertama
        throw new MyException();
        // sekumpulan kode kedua
    } catch (MyException e) {
        // sekumpulan kode ketiga
    }       
}

Kode semacam ini aneh, karena throw di sini digunakan untuk mengatur alur program, bukan untuk menangani kesalahan.

Pendekatan seperti ini membuat logika program jadi membingungkan, sulit diikuti, dan melanggar prinsip dasar dari pengecualian (exception) yang seharusnya hanya digunakan untuk menangani kondisi tak terduga, bukan untuk lompat-lompat antar bagian kode seperti goto.

Kalau memang tujuannya hanya mengatur urutan eksekusi, gunakan kontrol alur normal seperti if, return, atau break, bukan throw.


6 | Common Exceptions and Errors

Berikut beberapa pengecualian (exception) dan error umum yang sering ditemui dalam Java:

6.1 | Checked Exceptions**

  • IOException – Pengecualian ini biasanya menunjukkan bahwa terjadi kegagalan pada jaringan, sistem berkas (filesystem), atau database.

6.2 | Runtime Exceptions

  • ArrayIndexOutOfBoundsException – Terjadi ketika kita mencoba mengakses indeks array yang tidak ada, misalnya mencoba mengambil elemen indeks ke-5 dari array yang panjangnya hanya 3.

  • ClassCastException – Terjadi ketika kita mencoba melakukan konversi (cast) yang tidak sah, misalnya mengubah String menjadi List. Biasanya bisa dihindari dengan melakukan pemeriksaan instanceof sebelum melakukan casting.

  • IllegalArgumentException – Cara umum untuk menyatakan bahwa salah satu parameter yang diberikan ke metode atau konstruktor tidak valid.

  • IllegalStateException – Cara umum untuk menyatakan bahwa kondisi internal (seperti status objek) tidak valid.

  • NullPointerException – Terjadi ketika kita mencoba mengakses atau menggunakan objek yang bernilai null. Biasanya bisa dihindari dengan pemeriksaan null terlebih dahulu atau dengan menggunakan Optional.

  • NumberFormatException – Terjadi ketika kita mencoba mengonversi String menjadi angka, tetapi String tersebut mengandung karakter yang tidak valid, misalnya mencoba mengubah "5f3" menjadi angka.

6.3 | Errors

  • StackOverflowError – Terjadi ketika tumpukan (stack) terlalu besar. Kadang muncul pada aplikasi yang sangat besar, namun paling sering disebabkan oleh rekursi tanpa batas.

  • NoClassDefFoundError – Terjadi ketika sebuah kelas gagal dimuat, biasanya karena tidak ada di classpath atau gagal saat inisialisasi statis.

  • OutOfMemoryError – Terjadi ketika JVM kehabisan memori untuk membuat objek baru. Kadang disebabkan oleh kebocoran memori (memory leak).