Tipe-tipe standar menutup banyak kebutuhan, tetapi ada saatnya kegagalan kita terlalu khas untuk dilabeli sekadar std::runtime_error: handler ingin membedakan “saldo tidak cukup” dari kegagalan lain bukan lewat isi pesan, melainkan lewat tipe — dan ingin membaca datanya, misalnya jumlah kekurangannya. Di sinilah exception kustom masuk: class yang mewarisi std::exception atau salah satu turunannya, lalu menambah makna dan data domain sendiri. Mekanismenya persis inheritance yang sudah kita pelajari pada Section Pemrograman Berorientasi Objek; yang berbeda hanya kontrak yang harus dijaga, terutama pada what().
Dua Pola Mewarisi: Minimal dan Bertambah Data #
Pola pertama adalah yang paling ramping: mewarisi langsung std::exception dan meng-override what() agar mengembalikan pesan tetap. Pola ini cocok untuk kegagalan yang identitasnya sudah cukup, tanpa data tambahan. Pola kedua lebih umum dipakai: mewarisi std::runtime_error — yang sudah menyimpan pesan dan mengimplementasikan what() untuk kita — lalu menambahkan member data domain. Dengan pola kedua, exception tetap dikenali oleh handler catch (const std::runtime_error&) di kode lain, tetapi handler yang tahu tipenya bisa membaca datanya.
Ada satu kata kunci yang muncul di kedua pola: noexcept pada deklarasi what(). Ini bukan hiasan — class dasar std::exception mendeklarasikan what() const noexcept, sehingga override kita wajib menjaga janji yang sama: function ini tidak boleh melempar. Babak besar tentang noexcept kita bahas pada chapter tersendiri; untuk sekarang, ikuti kontraknya dan tambahkan override agar compiler memeriksa kesesuaian signature.
Exception yang Membawa Data Domain #
Program berikut memuat kedua pola sekaligus: KunciTidakValid yang minimal, dan SaldoTidakCukup yang membawa data perbankan — saldo saat ini dan kebutuhan transaksi — lengkap dengan pesan what() yang tersusun dari data itu:
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
// Pattern 1: turunan langsung dari std::exception, cukup satu pesan tetap.
class KunciTidakValid : public std::exception {
public:
const char* what() const noexcept override {
return "kunci konfigurasi tidak dikenal";
}
};
void muatKonfigurasi(const std::string& kunci) {
// Kunci yang sah hanya "tema" dan "bahasa".
if (kunci != "tema" && kunci != "bahasa") {
throw KunciTidakValid();
}
std::cout << " konfigurasi '" << kunci << "' dimuat\n";
}
// Pattern 2: turunan std::runtime_error dengan data domain tambahan.
class SaldoTidakCukup : public std::runtime_error {
public:
SaldoTidakCukup(double saldo, double kebutuhan)
: std::runtime_error("transaksi ditolak"),
saldo_(saldo),
kebutuhan_(kebutuhan),
pesan_("saldo " + std::to_string(saldo_) +
" lebih kecil dari kebutuhan " + std::to_string(kebutuhan_)) {}
// override what(): tetap noexcept sesuai kontrak class dasar.
const char* what() const noexcept override {
return pesan_.c_str(); // aman: pesan_ disimpan sebagai member
}
double saldo() const noexcept { return saldo_; }
double kebutuhan() const noexcept { return kebutuhan_; }
private:
double saldo_;
double kebutuhan_;
std::string pesan_; // data tambahan yang membawa konteks ke handler
};
class Rekening {
public:
explicit Rekening(double saldoAwal) : saldo_(saldoAwal) {}
void tarik(double jumlah) {
if (jumlah > saldo_) {
throw SaldoTidakCukup(saldo_, jumlah);
}
saldo_ -= jumlah;
std::cout << " penarikan " << jumlah << " berhasil, sisa " << saldo_
<< "\n";
}
private:
double saldo_;
};
int main() {
// Pattern 1: ditangani lewat class dasar, cukup membaca pesan.
try {
muatKonfigurasi("warna");
} catch (const std::exception& e) {
std::cout << "gagal muat konfigurasi: " << e.what() << "\n";
}
// Pattern 2: ditangani lewat tipe spesifiknya untuk membaca data domain.
Rekening rekening(500.0);
try {
rekening.tarik(250.0);
rekening.tarik(900.0);
} catch (const SaldoTidakCukup& e) {
std::cout << "gagal transaksi: " << e.what() << "\n";
std::cout << " saldo saat ini: " << e.saldo() << "\n";
std::cout << " kebutuhan: " << e.kebutuhan() << "\n";
std::cout << " kekurangan: " << e.kebutuhan() - e.saldo() << "\n";
}
return 0;
}Output yang Dihasilkan:
gagal muat konfigurasi: kunci konfigurasi tidak dikenal
penarikan 250 berhasil, sisa 250
gagal transaksi: saldo 250.000000 lebih kecil dari kebutuhan 900.000000
saldo saat ini: 250
kebutuhan: 900
kekurangan: 650Mari kita bedah bagian yang menentukan:
- Baris 6-11 (
KunciTidakValid): pola minimal —what()mengembalikan string literal, sehingga tidak ada member yang perlu disimpan dan tidak ada risiko pointer menggantung. - Baris 14-20 (
muatKonfigurasi): kunci"warna"tidak ada dalam daftar yang sah, jadithrow KunciTidakValid();dijalankan. - Baris 23-45 (
SaldoTidakCukup): constructor menyimpan dua data domain, lalu menyusunpesan_sekali di constructor — bukan setiap kaliwhat()dipanggil — dan menyimpannya sebagai memberstd::string. - Baris 63-70 (
catch (const SaldoTidakCukup& e)): karena kita menangkap lewat tipe spesifiknya, handler bisa memanggile.saldo()dane.kebutuhan()— data yang tidak dimiliki exception standar mana pun. Bariskekurangan: 650adalah jawaban yang sebenarnya dicari pemanggil. - Perbedaan format angka pada output:
what()menampilkan250.000000karenastd::to_string(double)selalu memakai enam desimal, sementarastd::coutmenampilkan250sesuai format bawaan stream — dua kebiasaan format yang berbeda, bukan data yang berbeda.
Kapan Custom Exception Layak Dibuat #
Membuat class exception baru adalah keputusan desain, bukan kebiasaan. Custom exception layak bila memenuhi salah satu syarat ini:
- Handler perlu membedakan kegagalan berdasarkan tipe, misalnya
catch (const SaldoTidakCukup&)di satu tempat dan penanganan generik di tempat lain — pemilihan berbasis tipe lebih andal daripada mem-parse isiwhat(). - Ada data domain yang dibutuhkan penangan: jumlah kekurangan saldo, indeks baris yang gagal, nama berkas yang bermasalah.
- Kegagalan itu punya makna domain yang stabil — istilah yang dipahami seluruh tim, seperti transaksi ditolak atau token kedaluwarsa.
Bila tidak ada syarat itu, jangan tambahkan class: throw std::runtime_error("pesan yang informatif"); sudah jujur dan cukup. Terlalu banyak tipe exception justru membuat peta penanganan bengkak tanpa manfaat. Satu panduan yang menyatukan semua ini: biarkan exception kustom Anda mewarisi std::runtime_error atau std::exception agar jaring pengaman catch (const std::exception&) — yang kita bahas pada chapter standar exceptions — tetap menampungnya.
Sekarang kita bisa melempar, menangkap, dan mendesain tipe exception sendiri. Pertanyaan berikutnya lebih dalam: ketika exception menyapu program di tengah operasi, apa yang terjadi pada state objek yang sedang dimodifikasi? Chapter berikutnya merumuskan tiga tingkat jaminan — basic, strong, dan noexcept — yang menjawabnya.
Referensi: cppreference — std::exception , cppreference — std::runtime_error .