Praktik Terbaik Exception

Larangan menelan exception dan menyaring catch yang bisa ditangani.

Daftar isi

Struktur try dan catch hanya menjadi aman ketika isinya mengikuti disiplin tertentu. Topik ini merangkum disiplin tersebut, yaitu larangan menelan exception tanpa catatan, prinsip menangani hanya exception yang memang bisa ditangani, serta kebiasaan melempar ulang yang tidak merusak jejaknya. Bagian akhir membahas pemisahan antara validasi masukan biasa dan kondisi yang benar-benar luar biasa, sehingga exception tidak menjadi alur kerja normal program. Setelah topik ini, setiap blok catch yang ditulis memiliki alasan yang jelas dan tidak menyembunyikan kegagalan.

Jangan pernah menelan exception#

Exception disebut ditelan ketika blok catch menghabiskannya tanpa aksi apa pun atau hanya mengembalikan nilai bawaan tanpa keterangan. Kegagalan yang ditelan membuat program tampak berjalan normal padahal datanya sudah tidak dapat dipercaya, dan penyebab aslinya tidak meninggalkan jejak apa pun. Bentuk paling buruk adalah catch yang badannya kosong, sedangkan bentuk yang lebih halus adalah catch yang diam-diam mengembalikan angka nol atau teks kosong. Keduanya memindahkan masalah dari waktu kegagalannya ke waktu yang jauh lebih sulit dilacak.

Program berikut membandingkan dua versi metode penguraian angka yang sama. Versi pertama menelan FormatException, sedangkan versi kedua mencatat kejadian sebelum mengembalikan nilai bawaan. Salin programnya ke berkas Program.cs, lalu jalankan dengan dotnet run --file Program.cs.

csharp Program.cs
// Program praktik terbaik: membedakan exception yang ditelan dan yang ditangani dengan catatan.
static int BacaAngkaSenyap(string teks)
{
    try
    {
        return int.Parse(teks);
    }
    catch (FormatException)
    {
        // Exception ditelan tanpa catatan apa pun.
        return 0;
    }
}

static int BacaAngkaTertangani(string teks)
{
    try
    {
        return int.Parse(teks);
    }
    catch (FormatException ex)
    {
        Console.WriteLine($"Ditangani: '{teks}' gagal diuraikan. Pesan asli: {ex.Message}");
        return 0;
    }
}

Console.WriteLine($"Hasil versi senyap: {BacaAngkaSenyap("tiga puluh")}");
Console.WriteLine($"Hasil versi tertangani: {BacaAngkaTertangani("tiga puluh")}");
text Keluaran
Hasil versi senyap: 0
Ditangani: 'tiga puluh' gagal diuraikan. Pesan asli: The input string 'tiga puluh' was not in a correct format.
Hasil versi tertangani: 0

Kedua versi mengembalikan angka nol untuk teks yang sama, tetapi hanya versi kedua yang meninggalkan pesan berisi teks masukan dan pesan asli exception. Pesan semacam itu tidak menyembuhkan kegagalan, tetapi menjadikannya dapat dianalisis dari catatan yang tampak. Bila tidak ada tindakan yang bermakna di blok catch tersebut, jangan menangkap exceptionnya sama sekali, dan biarkan ia naik ke pemanggil yang lebih memahami konteksnya.

Penangan pada batas aplikasi memang boleh memakai blok umum agar program interaktif tetap hidup, misalnya handler (fungsi penangan kejadian) pada antarmuka atau middleware (perangkat lunak perantara) pada server. Blok semacam itu tidak menelan exception, karena pesannya dicatat dan laporannya diberikan sebelum alur berlanjut. Perbedaannya terletak pada tindakannya, yaitu menangkap untuk melaporkan, bukan menangkap untuk mengabaikan.

Tangani hanya yang bisa ditangani#

Setiap blok catch wajib menjawab pertanyaan, yaitu tindakan apa yang dilakukan program setelah exception ini tertangkap. Tangkap hanya jenis yang maknanya diketahui dan tindakannya konkret, misalnya mencoba masukan lain, memakai nilai bawaan, atau membatalkan pekerjaan dengan rapi. Exception yang tidak dapat ditangani di lapisan tersebut dibiarkan naik ke pemanggil, dan penanganannya dilakukan pada lapisan yang benar-benar punya konteks.

Ketika lapisan dalam perlu mencatat kejadian tetapi tidak mampu memperbaikinya, gunakan throw tanpa operand untuk melempar ulang exception yang sama.

csharp Contoh: melempar ulang dengan throw tanpa operand
// throw; melempar kembali exception yang sama ke lapisan pemanggil.
try
{
    try
    {
        throw new InvalidOperationException("Stok tidak mencukupi.");
    }
    catch (InvalidOperationException)
    {
        Console.WriteLine("Lapisan dalam mencatat kejadian, lalu melempar ulang.");
        throw;
    }
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"Lapisan luar menerima: {ex.Message}");
}
text Keluaran
Lapisan dalam mencatat kejadian, lalu melempar ulang.
Lapisan luar menerima: Stok tidak mencukupi.

Baris throw; melempar objek exception yang sama ke lapisan atas tanpa mengubah objeknya. Sebaliknya, bentuk throw ex; memulai ulang stack trace, yaitu jejak tumpukan pemanggilan yang disimpan exception, dari baris pelemparan kedua. Lokasi kegagalan asli menjadi terputus, dan diagnosisnya lebih sulit. Aturan praktisnya, di dalam blok catch selalu gunakan throw; apabila exception ingin dilempar ulang.

Tempat penanganan yang tepat mengikuti tanggung jawab lapisan, bukan kebiasaan menaruh try di setiap metode. Metode tingkat bawah cukup melempar, lapisan tengah menambah konteks bila perlu, dan lapisan atas memutuskan tampilan serta tindak lanjutnya. Dengan pembagian ini, satu kegagalan hanya ditangani sekali, dan catatannya tidak terduplikasi di beberapa titik.

Pisahkan validasi dari kondisi luar biasa#

Exception dirancang untuk kondisi yang tidak biasa, bukan untuk jalur kerja yang memang diharapkan terjadi setiap saat. Melempar dan menangkap exception berbiaya jauh lebih besar daripada memeriksa kondisi, dan alur programnya menjadi sulit dibaca. Bila kegagalan adalah bagian normal dari masukan, misalnya teks dari pengguna yang bisa salah ketik, gunakan pola validasi seperti int.TryParse yang mengembalikan boolean beserta hasil penguraian.

csharp Contoh: validasi masukan dengan TryParse tanpa exception
// Prevalidasi dengan TryParse mencegah exception menjadi alur normal.
string[] masukan = { "42", "tujuh", "100" };

foreach (string teks in masukan)
{
    if (int.TryParse(teks, out int nilai))
    {
        Console.WriteLine($"{teks} -> {nilai}");
    }
    else
    {
        Console.WriteLine($"{teks} -> ditolak tanpa exception");
    }
}
text Keluaran
42 -> 42
tujuh -> ditolak tanpa exception
100 -> 100

Teks tujuh ditolak melalui pemeriksaan biasa tanpa exception sama sekali, sedangkan exception tetap diperuntukkan bagi keadaan yang benar-benar tidak terduga. Pemisahan ini menjaga log pemantauan tetap bermakna, karena setiap exception yang muncul adalah kejadian yang layak dicurigai, bukan sekadar salah ketik.

Keluarga metode TryParse bawaan .NET berlaku untuk angka, tanggal, dan format lain, sehingga pola yang sama dapat dipakai pada banyak jenis masukan. Untuk formulir dengan banyak kolom, kumpulkan seluruh kegagalan validasi terlebih dahulu, lalu laporkan bersamaan. Pendekatan itu lebih informatif daripada berhenti pada kesalahan pertama.

Kesalahan umum#

  • Blok catch (Exception) dipasang di setiap metode. Semua kegagalan tampak tertangani, tetapi bug seperti NullReferenceException ikut disembunyikan dan program berlanjut dalam keadaan rusak. Tangkap pada satu tempat yang tepat, biasanya pada batas aplikasi, dan biarkan exception lain naik.
  • Exception dilempar ulang dengan throw ex;. Jejak tumpukan aslinya terputus, sehingga baris kegagalan pertama tidak terbaca lagi. Gunakan throw; tanpa operand.
  • Logika penanganan ditempatkan pada catch yang tidak punya konteks. Metode tingkat bawah tidak mengetahui pesan apa yang pantas ditampilkan atau nilai apa yang aman dipakai ulang. Naikkan exception ke lapisan yang mengetahui konteksnya.

Ringkasan#

  • Exception tidak pernah ditelan diam-diam, karena bila ditangkap maka harus ada catatan atau tindakan yang jelas.
  • Tangkap hanya exception yang bisa ditangani dengan tindakan konkret, dan biarkan sisanya naik ke lapisan yang punya konteks.
  • Melempar ulang dengan throw; menjaga stack trace, sedangkan throw ex; memutus jejak kegagalan aslinya.
  • Kegagalan yang normal seperti salah ketik pengguna ditangani dengan validasi seperti int.TryParse, bukan dengan exception.

Referensi#