Menu

try-catch C++: menangani exception dengan cara yang benar

Bungkus kode yang berisiko di dalam try, lalu tanggapi di catch. Pelajari cara menangkap exception lewat reference const, mengurutkan banyak handler, memakai catch (...), dan melempar ulang tanpa membocorkan sumber daya.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Dari melempar ke menangani

Di halaman sebelumnya kamu belajar cara melakukan throw pada sebuah exception saat terjadi masalah. Melempar baru separuh cerita: exception yang tidak pernah ditangkap akan memanggil std::terminate dan membuat programmu crash. Pernyataan try/catch adalah cara kamu menangani apa yang dilempar dan tetap melanjutkan program.

Bentuknya sederhana: taruh kode yang berisiko di dalam blok try, lalu ikuti dengan satu atau beberapa blok catch yang menanggapi tipe error tertentu. Kalau blok try berjalan lancar, setiap catch dilewati. Begitu ada yang dilempar, kendali langsung melompat ke catch pertama yang cocok.

Perhatikan bahwa "after" tidak pernah dicetak. Begitu throw terjadi, sisa blok try ditinggalkan dan eksekusi dilanjutkan di dalam catch yang cocok. Setelah catch selesai, program berjalan normal di bawahnya.

Tangkap lewat reference const

Kebiasaan paling penting dalam penanganan error di C++: tangkap exception lewat reference const, bukan berdasarkan nilai.

Menangkap berdasarkan nilai menyalin exception-nya, dan lebih buruk lagi, memotongnya (slicing). Exception standar membentuk sebuah hierarki (runtime_error dan logic_error sama-sama diturunkan dari std::exception), jadi menangkap exception turunan sebagai nilai bertipe induk akan memotong bagian turunannya. Menangkap lewat reference menjaga objeknya tetap utuh dan polimorfik:

Di sini kita melempar out_of_range tapi menangkapnya sebagai const exception&. Karena out_of_range diturunkan dari exception, handler untuk class induk cocok, dan karena berupa reference, e.what() tetap mengembalikan pesan yang sebenarnya. Kalau kamu menulis catch (exception e) (berdasarkan nilai), objeknya akan terpotong menjadi exception biasa dan kamu bisa kehilangan pesan yang spesifik.

Banyak blok catch

Satu try bisa diikuti beberapa blok catch, masing-masing untuk tipe exception yang berbeda. C++ mencobanya dari atas ke bawah dan menjalankan yang pertama cocok, jadi urutkan dari yang paling spesifik sampai yang paling umum.

Karena invalid_argument lebih spesifik daripada exception, ia harus ditulis lebih dulu. Kalau urutannya kamu balik dan menaruh catch (const exception&) di atas, ia akan menelan setiap exception, dan handler invalid_argument di bawahnya menjadi kode mati yang tidak pernah bisa dijalankan. Banyak compiler memberi peringatan soal ini, tapi bahasanya sendiri tidak akan menghentikanmu.

catch (...) dan melempar ulang

Kadang kamu ingin jaring pengaman untuk apa pun yang tidak kamu duga. Handler penangkap semua, catch (...), cocok dengan setiap tipe exception, termasuk yang tidak diturunkan dari std::exception (seseorang bisa saja menulis throw 42; atau throw "oops";).

Masalahnya, kamu tidak mendapat objek apa pun: tidak ada e untuk diperiksa. Jadi catch (...) paling baik dipakai sebagai upaya terakhir: catat di log bahwa sesuatu gagal, atau bersihkan lalu lempar ulang.

Untuk melempar ulang exception yang sedang terjadi (meneruskannya ke handler di luar setelah melakukan pembersihan lokal atau mencatat log), pakai throw; polos tanpa operand. Cara ini mempertahankan exception aslinya (tipe dan pesan sebenarnya), berbeda dengan throw e; yang akan melempar ulang salinan yang sudah terpotong:

Handler di dalam mencatat log lalu melempar ulang; handler di luar, di main, kemudian menanganinya. Pakai throw; polos untuk ini, jangan pernah throw e;.

Stack unwinding dan RAII

Saat sebuah exception merambat keluar dari blok try, C++ melakukan stack unwinding: setiap objek lokal di antara throw dan catch yang cocok dipanggil destructor-nya, dalam urutan terbalik dari saat dibuat. Inilah yang membuat exception aman: sumber daya yang dipegang objek di stack dilepaskan secara otomatis.

Inilah alasan kamu sebaiknya memegang sumber daya dengan tipe RAII (seperti std::vector, std::string, dan smart pointer) daripada new/delete mentah. Perhatikan apa yang terjadi saat sebuah exception memotong alokasi manual:

void leaky() {
    int* buffer = new int[1000];
    mightThrow();        // if this throws, the next line never runs...
    delete[] buffer;     // ...and the buffer leaks
}

Karena throw melompati delete[], memorinya hilang. Smart pointer memperbaikinya secara gratis, karena destructor-nya dijalankan saat unwinding:

void safe() {
    auto buffer = std::make_unique<int[]>(1000);
    mightThrow();   // if this throws, buffer's destructor still frees the memory
}                   // no manual delete, no leak, even on the exception path

Kesimpulannya: jangan mencoba melakukan catch pada exception hanya untuk melakukan delete. Biarkan destructor yang membersihkan, dan simpan catch untuk keputusan tentang cara memulihkan program.

Kesalahan umum dan jebakan

Beberapa jebakan muncul berulang kali:

Jangan memakai exception untuk alur kendali biasa. Melempar dan unwinding jauh lebih lambat daripada if sederhana. Simpan exception untuk kondisi error yang benar-benar luar biasa, bukan untuk "pengguna mengetik string kosong".

Blok catch yang kosong menyembunyikan bug. Menulis catch (...) {} untuk membungkam error berarti kegagalan hilang tanpa jejak. Paling tidak, catat masalahnya di log; biasanya kamu sebaiknya melempar ulang atau menanganinya dengan benar.

Destructor yang melempar exception itu berbahaya. Kalau sebuah destructor melempar exception selama stack unwinding (saat exception lain sedang berjalan), program akan memanggil std::terminate. Di C++ modern, destructor secara implisit bersifat noexcept, jadi jangan pernah biarkan exception lolos dari destructor.

catch hanya melihat apa yang dicakup try. Exception yang dilempar sebelum masuk ke try, atau di fungsi lain yang tidak berada di jalur pemanggilan di dalamnya, tidak akan ditangkap di sini. catch hanya melindungi kode yang berjalan di dalam blok try miliknya sendiri (secara langsung atau di fungsi-fungsi yang dipanggilnya).

Berikutnya: undefined behavior

Exception adalah cara C++ yang terdefinisi untuk memberi tahu bahwa ada yang salah: kamu melempar, kamu menangkap, perilakunya bisa ditebak. Tapi C++ juga punya sudut gelap tempat bahasa ini sama sekali tidak berjanji apa pun: dereference dangling pointer, membaca melewati ujung array, overflow pada signed integer. Halaman berikutnya membahas undefined behavior: apa pemicunya, kenapa ia bisa kelihatan "berjalan" sampai akhirnya gagal total, dan cara menjauhkannya dari kodemu.

Pertanyaan yang sering diajukan

Bagaimana cara kerja try-catch di C++?

Kamu menaruh kode yang mungkin melempar exception di dalam blok try { }. Kalau sebuah exception dilempar, program berhenti menjalankan sisa blok try lalu melompat ke blok catch pertama yang cocok, tempat kamu menangani error-nya. Kalau tidak ada yang dilempar, blok-blok catch dilewati sepenuhnya.

Kenapa sebaiknya menangkap exception lewat reference const di C++?

Menangkap lewat reference (catch (const std::exception& e)) menghindari penyalinan objek exception dan, yang paling penting, mempertahankan polimorfisme: exception turunan yang ditangkap sebagai tipe induknya tetap memanggil what() yang benar. Menangkap berdasarkan nilai (catch (std::exception e)) memotong bagian turunannya dan bisa menghilangkan informasi.

Bagaimana cara menangkap exception apa pun di C++?

Pakai catch (...): elipsis itu menangkap setiap exception apa pun tipenya. Ini berguna sebagai handler upaya terakhir, tapi karena kamu tidak mendapat objek untuk diperiksa, taruh setelah blok-blok catch yang spesifik dan pakai terutama untuk mencatat log atau melempar ulang.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai