try/catch/finally

Alur exception, exception filter, dan catch spesifik vs umum.

Daftar isi

Program yang benar saat ditulis tetap dapat menemui keadaan yang tidak dapat dilanjutkan, misalnya pembagian dengan nol atau teks yang gagal diuraikan menjadi angka. Untuk keadaan semacam itu, .NET menyediakan exception, yaitu objek yang dibuat dan dilempar (throw) ketika error terjadi, kemudian bergerak melintasi rantai pemanggilan metode sampai ditemukan penangan yang cocok. Topik ini membahas struktur try, catch, dan finally, urutan blok catch dari yang spesifik ke yang umum, serta filter, yaitu kondisi tambahan pada sebuah blok catch. Setelah topik ini dipahami, kode yang berisiko gagal dapat dibungkus penanganan yang tepat, dan pembersihan akhir tetap dijamin berjalan.

Alur exception#

Exception adalah objek dari kelas yang menurunkan System.Exception, dan setiap error saat program berjalan dilaporkan melalui objek semacam itu. Ketika sebuah exception dilempar, runtime, yaitu lapisan yang menjalankan program C#, mencari blok catch yang cocok pada metode yang sedang berjalan. Apabila metode tersebut tidak memilikinya, pencarian berpindah ke metode pemanggilnya, dan seterusnya sampai ujung call stack, yakni rantai metode yang sedang bersarang saat program berjalan. Exception yang tidak pernah tertangkap mencetak laporan error lalu menghentikan program.

Struktur try dan catch membungkus kode yang berpotensi melempar exception. Baris di dalam blok try dijalankan sesuai urutan normal, tetapi begitu sebuah exception dilempar, sisa pernyataan pada blok tersebut diloncati dan eksekusi berpindah ke blok catch yang tipenya cocok. Program berikut menampilkan kedua jalur tersebut untuk tiga pembagi yang berbeda. Salin programnya ke berkas Program.cs, lalu jalankan dengan dotnet run --file Program.cs.

csharp Program.cs
// Program dasar try/catch/finally: alur normal, exception tertangkap, dan finally.
int[] daftarPembagi = { 2, 0, 4 };

foreach (int pembagi in daftarPembagi)
{
    try
    {
        Console.WriteLine($"Hasil 10 / {pembagi} = {10 / pembagi}");
    }
    catch (DivideByZeroException ex)
    {
        Console.WriteLine($"Exception tertangkap: {ex.Message}");
    }
    finally
    {
        Console.WriteLine($"Percobaan dengan pembagi {pembagi} selesai.");
    }
}
text Keluaran
Hasil 10 / 2 = 5
Percobaan dengan pembagi 2 selesai.
Exception tertangkap: Attempted to divide by zero.
Percobaan dengan pembagi 0 selesai.
Hasil 10 / 4 = 2
Percobaan dengan pembagi 4 selesai.

Pembagi 2 dan 4 mengikuti jalur normal, yaitu pernyataan di dalam try dijalankan penuh lalu finally menyusul. Pembagi 0 memicu DivideByZeroException pada pernyataan pembagian, sehingga pernyataan yang menampilkan hasil tidak pernah dijalankan dan eksekusi langsung berpindah ke blok catch. Blok finally tetap dieksekusi pada ketiga percobaan, dan urutan keluarannya memperlihatkan hal itu.

Exception tidak wajib ditangani di metode tempat ia muncul. Bila metode yang sedang berjalan tidak menyediakan blok catch yang cocok, exception naik ke pemanggilnya beserta seluruh isinya. Program berikut menangkap exception yang dilempar di dalam metode lain.

csharp Contoh: exception bergerak naik ke pemanggil
// Exception yang tidak ditangani di dalam metode bergerak naik ke pemanggilnya.
static int KaliSeratus(int pembagi) => 100 / pembagi;

try
{
    Console.WriteLine(KaliSeratus(0));
}
catch (DivideByZeroException ex)
{
    Console.WriteLine($"Tertangkap di pemanggil: {ex.Message}");
}
text Keluaran
Tertangkap di pemanggil: Attempted to divide by zero.

Beberapa blok catch#

Satu blok try dapat diikuti beberapa blok catch, masing-masing untuk satu jenis exception. Blok-blok tersebut diperiksa dari atas ke bawah, dan blok pertama yang jenisnya cocok dengan exception yang dilempar itulah yang dijalankan. Blok paling umum adalah catch (Exception), karena semua exception menurunkan kelas tersebut. Karena pencocokannya berurutan, blok yang spesifik wajib ditulis lebih dahulu, sedangkan blok umum ditempatkan terakhir sebagai penampung sisanya.

csharp Contoh: catch spesifik di atas, catch umum di bawah
// Catch spesifik ditulis lebih dahulu, catch umum menampung sisanya.
foreach (string teks in new[] { "12", "ab" })
{
    try
    {
        Console.WriteLine($"Angka: {int.Parse(teks)}");
    }
    catch (FormatException ex)
    {
        Console.WriteLine($"Format salah: {ex.Message}");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Exception lain: {ex.Message}");
    }
}
text Keluaran
Angka: 12
Format salah: The input string 'ab' was not in a correct format.

Teks ab memicu FormatException, sehingga blok pertama menangkapnya dan pesan aslinya dicetak. Bila jenis exceptionnya bukan FormatException, misalnya OverflowException, blok pertama tidak cocok dan penanganan jatuh ke catch (Exception). Blok umum semacam itu sebaiknya hanya mencatat kejadian, lalu melempar ulang atau menghentikan program, karena tindakan spesifik tidak mungkin ditentukan di sana.

finally#

Blok finally menampung pernyataan yang wajib dijalankan apa pun yang terjadi pada blok try, baik berakhir normal, berakhir dengan exception tertangkap, maupun berakhir dengan return. Runtime menjamin blok ini dieksekusi sebelum alur program berlanjut atau sebelum exception naik ke pemanggil. Karena jaminan itu, finally adalah tempat pembersihan sumber daya seperti penutupan berkas atau koneksi jaringan.

csharp Contoh: finally tetap berjalan meski try berakhir dengan return
// finally tetap dijalankan meskipun try berakhir dengan return.
static int Contoh()
{
    try
    {
        Console.WriteLine("Blok try selesai dengan return.");
        return 7;
    }
    finally
    {
        Console.WriteLine("Blok finally tetap berjalan.");
    }
}

Console.WriteLine($"Nilai kembali: {Contoh()}");
text Keluaran
Blok try selesai dengan return.
Blok finally tetap berjalan.
Nilai kembali: 7

Blok try pada program di atas berakhir dengan return 7, tetapi baris pada finally tetap tercetak sebelum nilai kembali diterima pemanggil. Urutan keluarannya menegaskan bahwa finally bukan bagian dari blok try, melainkan langkah penutup yang dijalankan runtime setelah eksekusi blok try selesai. Pola try dan finally inilah yang menjadi dasar pernyataan using dalam pembersihan sumber daya.

Exception filter dengan when#

Blok catch dapat dipasangi filter, yaitu ekspresi boolean yang ditulis dengan kata kunci when setelah tipe exception. Filter mempersempit satu jenis exception menjadi beberapa penangan sesuai kondisinya, tanpa perlu memeriksa properti di dalam blok lalu melempar ulang. Blok catch dengan filter hanya cocok apabila tipenya cocok dan ekspresi filternya bernilai true. Bila filter bernilai false, pencarian blok catch berlanjut ke blok berikutnya seolah filter itu tidak ada.

csharp Contoh: filter when pada blok catch
// Exception filter mempersempit catch tanpa menambah jenis blok baru.
foreach (int nilai in new[] { -5, 150 })
{
    try
    {
        Periksa(nilai);
        Console.WriteLine($"Nilai {nilai} diterima.");
    }
    catch (ArgumentOutOfRangeException ex) when (ex.ParamName == "nilai")
    {
        Console.WriteLine($"Filter pertama: {ex.Message}");
    }
    catch (ArgumentOutOfRangeException ex)
    {
        Console.WriteLine($"Filter kedua: {ex.Message}");
    }
}

static void Periksa(int nilai)
{
    if (nilai < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(nilai), "Nilai tidak boleh negatif.");
    }
    if (nilai > 100)
    {
        throw new ArgumentOutOfRangeException("batas", "Nilai melebihi 100.");
    }
}
text Keluaran
Filter pertama: Nilai tidak boleh negatif. (Parameter 'nilai')
Filter kedua: Nilai melebihi 100. (Parameter 'batas')

Nilai -5 ditolak dengan parameter bernama nilai, sehingga filter pertama yang memeriksa ex.ParamName cocok dan menangkapnya. Nilai 150 juga melempar ArgumentOutOfRangeException, tetapi nama parameternya adalah batas, sehingga filter pertama bernilai false dan penanganan jatuh ke blok kedua. Perilaku ini berguna ketika satu jenis exception membawa beberapa keadaan yang penanganannya berbeda.

Kesalahan umum#

  • Blok catch umum ditempatkan sebelum blok spesifik. Kompilator menolaknya dengan error CS0160 karena blok terakhir menjadi tidak dapat dicapai, sebagaimana contoh berikut. Perbaikannya adalah menukar urutannya sehingga blok spesifik berada di atas.
  • Pembersihan ditulis di akhir blok try alih-alih di finally. Baris setelah pernyataan yang melempar exception tidak pernah dijalankan, sehingga sumber daya tidak tertutup. Pindahkan pembersihannya ke finally, atau gunakan pernyataan using.
  • catch (Exception) dipakai untuk semua keadaan. Semua jenis error tampak tertangani, tetapi bug pemrograman seperti NullReferenceException ikut disembunyikan dan program berlanjut dalam keadaan rusak. Tangkap jenis yang maknanya diketahui, dan biarkan sisanya naik.
csharp Contoh: error CS0160 karena catch umum di atas catch spesifik
// tidak-dijalankan
// Kompilator menolak urutan ini dengan error CS0160, karena catch (Exception)
// di atas sudah menangkap semua exception, termasuk FormatException.
try
{
    int.Parse("12");
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}
catch (FormatException ex)
{
    Console.WriteLine(ex.Message);
}

Ringkasan#

  • Exception bergerak melintasi rantai pemanggilan sampai menemukan blok catch yang cocok, dan program berhenti bila tidak ada yang menangkapnya.
  • Beberapa blok catch diperiksa dari atas ke bawah, sehingga blok spesifik ditulis sebelum blok umum seperti catch (Exception).
  • Blok finally dijamin dijalankan pada semua akhir blok try, sehingga menjadi tempat pembersihan sumber daya.
  • Filter when menambahkan kondisi pada blok catch, dan bila filternya false, pencarian berlanjut ke blok berikutnya.

Referensi#