Kita sudah mampu melempar dan menangkap, tetapi melempar int polos tentu bukan gaya yang akan kita pertahankan. Pustaka standar C++ menyediakan satu hierarki class exception yang siap pakai — semua turunan dari std::exception — sehingga kita tidak perlu mengarang tipe sendiri untuk kebutuhan yang umum. Menggunakan tipe standar punya keuntungan ganda: pustaka lain mengenalinya, dan satu handler catch (const std::exception&) bisa menjadi jaring pengaman untuk semuanya. Chapter ini berkeliling di hierarki tersebut, membuktikan pesan bawaannya dengan program nyata, lalu merumuskan aturan memilih tipe yang tepat.

Hierarki std::exception #

Di puncak berdiri std::exception dengan satu function virtual penting: what(), yang mengembalikan const char* berisi deskripsi singkat kegagalan dan dijamin tidak melempar exception. Dari sana, tipe-tipe standar terbagi ke beberapa cabang:

Semua nama pada diagram berawalan std::, misalnya std::exception dan std::logic_error. Dua keluarga besar di tengah mewakili dua dunia kegagalan yang berbeda. Keluarga logic_error adalah kesalahan pemrograman — kontrak yang dilanggar sebelum program sempat berjalan, seperti argumen yang tidak valid atau indeks di luar jangkauan; secara ideal kegagalan jenis ini ditemukan saat pengujian. Keluarga runtime_error adalah kegagalan yang tak sepenuhnya di kendalikan kode — keadaan lingkungan yang berubah, seperti penuhnya memori atau hilangnya sumber data. Sementara std::bad_alloc dan std::bad_cast lahir dari operasi bahasa: new yang gagal alokasi dan dynamic_cast ke tipe reference yang tidak cocok.

what(): Pesan Bawaan Setiap Exception #

Program berikut memicu empat exception standar yang berbeda dari satu function, lalu menangkap semuanya dengan satu handler yang parameternya bertipe class dasar — bukti bahwa hierarki ini bekerja lewat polymorphism:

cpp
#include <iostream>
#include <stdexcept>
#include <string>
#include <typeinfo>
#include <vector>

// Satu function yang bisa memicu empat exception standar berbeda,
// dipilih lewat parameternya.
void picu(int nomor) {
    std::vector<int> stok{1, 2, 3};

    if (nomor == 1) {
        int x = stok.at(99);          // std::out_of_range dari at()
        (void)x;
    } else if (nomor == 2) {
        int n = std::stoi("lima");    // std::invalid_argument dari stoi()
        (void)n;
    } else if (nomor == 3) {
        struct Dasar { virtual ~Dasar() = default; };
        struct Turunan : Dasar {};
        Dasar d;
        Dasar& r = dynamic_cast<Turunan&>(d);   // std::bad_cast
        (void)r;
    } else {
        throw std::runtime_error("kertas printer habis");
    }
}

int main() {
    const char* nama[] = {"out_of_range", "invalid_argument", "bad_cast",
                          "runtime_error"};

    for (int i = 0; i < 4; ++i) {
        try {
            picu(i + 1);
        } catch (const std::exception& e) {
            // Satu handler untuk semua: tipe parameter adalah class dasar.
            std::cout << nama[i] << ".what(): " << e.what() << "\n";
        }
    }
    return 0;
}

Output yang Dihasilkan:

text
out_of_range.what(): vector::_M_range_check: __n (which is 99) >= this->size() (which is 3)
invalid_argument.what(): stoi
bad_cast.what(): std::bad_cast
runtime_error.what(): kertas printer habis

Poin-poin kuncinya:

  1. Baris 13 (stok.at(99)): at() memeriksa indeks dan melempar std::out_of_range; bandingkan dengan stok[99] yang tidak memeriksa apa pun dan berujung undefined behavior. Pesan what()-nya bahkan menyebut indeks 99 dan ukuran 3.
  2. Baris 16 (std::stoi("lima")): konversi string ke angka yang gagal melempar std::invalid_argument dengan pesan singkat "stoi".
  3. Baris 20-22 (dynamic_cast<Turunan&>(d)): cast ke tipe reference yang salah melempar std::bad_cast — ini satu-satunya cara dynamic_cast melaporkan kegagalan pada reference, karena tidak ada nullptr yang bisa dikembalikan.
  4. Baris 26 (throw std::runtime_error(...)): pesan yang kita tulis sendiri muncul utuh lewat what().
  5. Baris 44 (catch (const std::exception& e)): satu handler menangkap empat tipe berbeda karena semuanya turunan std::exception; pemanggilan e.what() menyelesaikan ke function yang benar lewat virtual dispatch.

Perlu dicatat: teks what() bawaan pustaka standar (seperti vector::_M_range_check...) adalah milik implementasi libstdc++ GCC 14 — deterministik di lingkungan yang sama, tetapi tidak dijamin identitas antar compiler. Karena itu tangani exception berdasarkan tipenya, dan perlakukan what() sebagai pesan untuk manusia, bukan data yang di-parse.

Memilih Exception yang Tepat #

Aturan praktisnya sederhana: pilih tipe paling spesifik yang deskripsinya cocok, dan jangan melempar std::exception mentah-mentah karena tipenya tidak memberi informasi apa pun. Tabel berikut merangkum pilihan yang paling sering dipakai:

ExceptionHeaderKapan tepat dipakai
std::invalid_argument<stdexcept>Argumen yang diterima tidak valid, mis. string yang seharusnya angka
std::out_of_range<stdexcept>Indeks atau key di luar jangkauan (at())
std::length_error<stdexcept>Ukuran melebihi batas maksimum container/string
std::runtime_error<stdexcept>Kegagalan lingkungan saat berjalan; dasar yang baik untuk exception kustom
std::overflow_error<stdexcept>Hasil aritmetika melampaui jangkauan tipe
std::bad_alloc<new>new gagal mengalokasikan memori
std::bad_cast<typeinfo>dynamic_cast ke reference gagal

Satu pola terakhir yang layak dibiasakan: pada batas program — main(), handler request, atau fungsi run() paling luar — pasang satu catch (const std::exception&) sebagai jaring pengaman agar kegagalan apa pun setidaknya tercatat sebelum program memutuskan berhenti. Dengan begitu exception kustom sekalipun tidak lolos tak tercatat. Bagaimana mendesain exception kustom yang membawa data domain sendiri? Itulah topik chapter berikutnya.

Referensi: cppreference — std::exception , cppreference — std::out_of_range , cppreference — std::runtime_error .