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:

cpp
#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:

text
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: 650

Mari kita bedah bagian yang menentukan:

  1. Baris 6-11 (KunciTidakValid): pola minimal — what() mengembalikan string literal, sehingga tidak ada member yang perlu disimpan dan tidak ada risiko pointer menggantung.
  2. Baris 14-20 (muatKonfigurasi): kunci "warna" tidak ada dalam daftar yang sah, jadi throw KunciTidakValid(); dijalankan.
  3. Baris 23-45 (SaldoTidakCukup): constructor menyimpan dua data domain, lalu menyusun pesan_ sekali di constructor — bukan setiap kali what() dipanggil — dan menyimpannya sebagai member std::string.
  4. Baris 63-70 (catch (const SaldoTidakCukup& e)): karena kita menangkap lewat tipe spesifiknya, handler bisa memanggil e.saldo() dan e.kebutuhan() — data yang tidak dimiliki exception standar mana pun. Baris kekurangan: 650 adalah jawaban yang sebenarnya dicari pemanggil.
  5. Perbedaan format angka pada output: what() menampilkan 250.000000 karena std::to_string(double) selalu memakai enam desimal, sementara std::cout menampilkan 250 sesuai 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 isi what().
  • 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 .