Foto oleh Ann H via Pexels

Exception handling adalah mekanisme untuk menangani kondisi error yang terjadi saat program berjalan, mencegah aplikasi crash secara tiba-tiba dan memberikan kesempatan untuk merespons kesalahan tersebut dengan cara yang lebih terkendali.

C# menyediakan blok try-catch-finally untuk menangani exception: kode yang berpotensi menghasilkan error ditulis di dalam try, penanganan error ditulis di dalam catch, dan kode yang harus selalu dijalankan apapun hasilnya (berhasil atau gagal) ditulis di dalam finally.

Artikel ini membahas cara menulis try-catch-finally yang efektif, menangkap jenis exception spesifik dibanding exception umum, serta kapan membuat custom exception untuk kebutuhan aplikasi yang lebih spesifik.

Struktur Dasar Try-Catch-Finally

Finally selalu dijalankan baik terjadi exception maupun tidak

try { int hasil = 10 / int.Parse("0"); } catch (DivideByZeroException ex) { Console.WriteLine($"Error: {ex.Message}"); } finally { Console.WriteLine("Blok ini selalu dijalankan"); }
Blok finally akan selalu dieksekusi setelah try atau catch selesai, baik exception terjadi maupun tidak, menjadikannya tempat yang tepat untuk kode pembersihan seperti menutup koneksi database atau file yang harus selalu dilakukan apapun hasil dari blok try tersebut.

Menangkap Exception Spesifik vs Exception Umum

Menangkap exception spesifik memungkinkan respons berbeda per kasus

try { string[] data = File.ReadAllLines("data.txt"); } catch (FileNotFoundException ex) { Console.WriteLine("File tidak ditemukan, membuat file baru"); } catch (UnauthorizedAccessException ex) { Console.WriteLine("Tidak punya izin akses file"); } catch (Exception ex) { Console.WriteLine($"Error tidak terduga: {ex.Message}"); }
Menangkap jenis exception yang lebih spesifik terlebih dahulu memungkinkan program memberikan respons yang berbeda sesuai akar masalahnya, sementara catch Exception umum di posisi terakhir berfungsi sebagai jaring pengaman untuk kasus yang tidak terduga. Urutan catch block ini penting karena C# mengevaluasi dari atas ke bawah dan berhenti pada yang pertama cocok.

Membuat Custom Exception

Custom exception memberi nama yang bermakna sesuai domain bisnis

public class SaldoTidakCukupException : Exception { public SaldoTidakCukupException(string pesan) : base(pesan) { } } public void Tarik(decimal jumlah) { if (jumlah > saldo) throw new SaldoTidakCukupException("Saldo tidak mencukupi untuk penarikan ini"); saldo -= jumlah; }
Custom exception yang diwariskan dari class Exception memungkinkan error yang dilempar memiliki nama yang jelas dan bermakna sesuai domain bisnis aplikasi, seperti SaldoTidakCukupException, jauh lebih deskriptif dibanding melempar Exception generik yang tidak menjelaskan konteks kesalahan yang sebenarnya terjadi.

Rethrow Exception dengan Benar

throw tanpa objek mempertahankan stack trace asli

try { ProsesTransaksi(); } catch (Exception ex) { LogError(ex); throw; // MEMPERTAHANKAN stack trace asli // throw ex; -- HINDARI ini, merusak stack trace asli }
Ketika exception perlu dilempar ulang setelah dicatat (logged), menulis throw saja tanpa menyertakan objek exception akan mempertahankan stack trace asli yang menunjukkan lokasi sebenarnya error terjadi, sementara menulis throw ex akan mereset stack trace seolah error terjadi di titik rethrow tersebut, menyulitkan proses debugging.

Exception Filter dan Urutan Catch Block yang Tepat

Exception filter dengan 'when' dan urutan catch dari spesifik ke umum

try { int[] data = { 1, 2, 3 }; Console.WriteLine(data[5]); } catch (IndexOutOfRangeException ex) when (ex.Message.Contains("index")) { Console.WriteLine("Index di luar jangkauan: " + ex.Message); } catch (Exception ex) { Console.WriteLine("Error umum: " + ex.Message); } finally { Console.WriteLine("Proses selesai"); } // Output: // Index di luar jangkauan: Index was outside the bounds of the array. // Proses selesai

Urutan penulisan blok catch pada C# sangat berpengaruh karena runtime akan mengevaluasinya dari atas ke bawah dan berhenti pada blok pertama yang cocok dengan tipe exception yang terjadi. Oleh karena itu, blok catch untuk tipe exception yang lebih spesifik seperti IndexOutOfRangeException harus dituliskan sebelum blok catch untuk tipe yang lebih umum seperti Exception; jika urutannya terbalik, compiler akan menampilkan error karena blok catch spesifik menjadi tidak pernah bisa tercapai (unreachable).

Fitur exception filter yang ditandai dengan kata kunci when memungkinkan sebuah blok catch hanya dijalankan jika kondisi tambahan di dalam when bernilai true, selain tipe exception-nya sudah cocok. Jika kondisi when bernilai false, runtime akan melanjutkan pencarian ke blok catch berikutnya seolah-olah blok tersebut tidak pernah ada, memberikan fleksibilitas menangani exception yang sama dengan cara berbeda tergantung isi pesannya atau properti lain dari exception tersebut.

Blok finally akan selalu dieksekusi terlepas dari apakah exception terjadi atau tidak, bahkan ketika ada statement return di dalam blok try atau catch, sehingga blok ini ideal dipakai untuk melepas resource seperti menutup koneksi database atau file stream yang wajib dijalankan dalam kondisi apapun.

Membuat Custom Exception dan Exception Wrapping dengan InnerException

Custom exception membawa data tambahan spesifik domain aplikasi

public class SaldoTidakCukupException : Exception { public decimal SaldoTersedia { get; } public SaldoTidakCukupException(string pesan, decimal saldoTersedia) : base(pesan) { SaldoTersedia = saldoTersedia; } } try { try { throw new InvalidOperationException("Koneksi payment gateway gagal"); } catch (Exception ex) { throw new SaldoTidakCukupException("Transaksi ditolak", 50000) { // InnerException otomatis tersimpan lewat constructor base jika diteruskan }; } } catch (SaldoTidakCukupException ex) { Console.WriteLine($"{ex.Message}, sisa saldo: {ex.SaldoTersedia}"); } // Output: Transaksi ditolak, sisa saldo: 50000

Membuat custom exception dengan menurunkan class dari Exception memungkinkan sebuah error domain-specific dibawa beserta data kontekstual tambahan yang relevan, seperti SaldoTersedia pada SaldoTidakCukupException di atas, sesuatu yang tidak bisa dilakukan jika hanya mengandalkan exception bawaan seperti InvalidOperationException yang hanya menyimpan pesan teks generik. Konvensi penamaan custom exception di C# selalu diakhiri kata Exception agar mudah dikenali jenisnya dari daftar using maupun saat membaca stack trace.

Exception wrapping adalah teknik menangkap exception tingkat rendah dan melemparkannya kembali sebagai exception tingkat lebih tinggi yang lebih bermakna bagi pemanggil, sambil tetap menyertakan exception asli melalui parameter innerException pada constructor base Exception agar informasi detail teknis tidak hilang dan tetap bisa ditelusuri melalui property InnerException saat proses debugging, meskipun pesan yang ditampilkan ke pengguna akhir sudah disederhanakan.

Praktik yang baik dalam membuat custom exception adalah selalu menyediakan beberapa overload constructor yang mengikuti pola constructor pada class Exception bawaan, yaitu tanpa parameter, dengan parameter message saja, dan dengan parameter message beserta innerException, sehingga custom exception tersebut tetap kompatibel dengan tooling logging dan framework yang mengharapkan pola constructor standar tersebut. Menandai custom exception dengan atribut [Serializable] juga masih relevan pada aplikasi yang mendukung remoting atau komunikasi antar proses lama, meskipun pada aplikasi web modern berbasis ASP.NET Core kebutuhan ini sudah jauh berkurang. Memberi custom exception property tambahan yang relevan, seperti SaldoTersedia di atas, jauh lebih membantu proses debugging dibanding hanya mengandalkan teks pesan error yang kadang kurang terstruktur untuk diproses secara otomatis oleh sistem logging.

Blok finally dan Jaminan Eksekusi Cleanup Code

StreamReader? reader = null; try { reader = new StreamReader("data.txt"); Console.WriteLine(reader.ReadLine()); } catch (FileNotFoundException) { Console.WriteLine("File tidak ditemukan"); } finally { reader?.Dispose(); Console.WriteLine("Proses selesai, resource dibersihkan"); } // Output (jika file tidak ada): // File tidak ditemukan // Proses selesai, resource dibersihkan

Blok finally pada struktur try-catch selalu dijalankan setelah blok try atau catch selesai, baik exception terjadi maupun tidak, bahkan ketika ada statement return di dalam try yang seharusnya langsung mengakhiri method. Karakteristik ini menjadikan finally tempat yang tepat untuk kode pembersihan seperti menutup koneksi, melepas file handle, atau melepas lock yang wajib dijalankan apa pun hasil akhir dari blok try tersebut, tanpa ada kemungkinan terlewat sama sekali.

Meskipun statement using umumnya lebih disukai untuk object yang mengimplementasikan IDisposable karena sintaksnya lebih ringkas dan tidak mudah lupa ditulis, finally tetap relevan untuk logika cleanup yang lebih kompleks atau melibatkan beberapa resource berbeda sekaligus yang tidak semuanya mengimplementasikan IDisposable. Finally juga sering dipakai untuk pencatatan log bahwa suatu proses telah selesai dieksekusi tanpa memandang hasil akhirnya, baik berhasil maupun gagal karena exception yang tidak tertangani sekalipun.

Kesalahan Umum yang Sering Terjadi

Menangkap Exception Umum Tanpa Menanganinya dengan Benar

Menangkap catch (Exception ex) lalu membiarkan blok catch kosong tanpa logging atau penanganan apa pun akan menyembunyikan error yang sebenarnya terjadi, membuat debugging masalah produksi menjadi sangat sulit di kemudian hari.

Memakai Exception untuk Alur Logika Normal

Menggunakan exception untuk mengontrol alur program yang sebenarnya merupakan kondisi normal, seperti validasi input yang gagal, lebih mahal secara performa dibanding penanganan dengan if-else biasa dan mengaburkan makna exception sebagai penanda kondisi yang benar-benar tidak terduga.

Menulis throw ex yang Merusak Stack Trace

Rethrow dengan throw ex akan menghilangkan informasi lokasi asli terjadinya error, membuat log error menunjukkan lokasi yang salah dan menyulitkan proses investigasi akar masalah sebenarnya.

Kapan Pendekatan Ini Tidak Cocok Dipakai

Menangkap exception yang sangat spesifik memberikan kontrol penanganan error yang presisi untuk setiap kasus, namun membutuhkan lebih banyak kode catch block dibanding sekadar menangkap Exception umum sekali saja.

Custom exception memberikan kejelasan semantik yang besar untuk error spesifik domain bisnis, namun membuat terlalu banyak jenis custom exception untuk kasus yang sangat spesifik bisa membuat kode penanganan error menjadi rumit dan sulit diikuti jika tidak dikelola dengan hierarki yang baik.

Ringkasan

Exception handling yang baik melibatkan penangkapan exception spesifik untuk respons yang tepat sasaran, custom exception untuk kejelasan domain bisnis, dan rethrow yang benar untuk menjaga informasi debugging tetap utuh. Hindari memakai exception untuk alur logika normal, dan selalu tangani exception dengan penanganan yang bermakna, bukan sekadar menangkapnya lalu diabaikan.

Seri Belajar C# Fundamental:

← Sebelumnya: Interface vs Abstract Class di C#

→ Selanjutnya: Collection Generic List Dictionary dan HashSet