Sepuluh chapter sebelumnya menyusun perangkat yang lengkap: melempar dan menangkap, hierarki standard exceptions, custom exceptions, jaminan exception safety, noexcept, error_code, dan std::expected. Semua itu bermuara pada satu keputusan desain yang kita hadapi di hampir setiap function yang bisa gagal: laporkan lewat exception, atau lewat nilai kembalian? Chapter penutup section ini menjawabnya dengan kerangka yang ringkas — exception untuk kegagalan luar biasa, expected dan error_code untuk kegagalan yang diharapkan — lalu membuktikan bahwa kedua gaya menghasilkan program yang sama benarnya, hanya berbeda struktur dan biaya.

Pertanyaan Intinya: Diharapkan atau Luar Biasa #

Semua kegagalan bukan kelas yang sama. Ada kegagalan yang diharapkan (expected failures): input pengguna salah format, berkas konfigurasi belum ada, jaringan sempat terputus, kunci cache belum terisi. Kegagalan semacam ini adalah bagian normal dari pemakaian program — kita bahkan merancang untuknya, penanganannya tinggal dekat dengan titik kejadian, dan ia bisa terjadi puluhan kali dalam satu menit.

Ada pula kegagalan luar biasa (exceptional failures): invariant class dilanggar, pointer yang semestinya selalu valid ternyata nullptr, memori habis (std::bad_alloc), atau kondisi yang membuat melanjutkan tidak bermakna. Kejadian ini seharusnya jarang, layak dicatat ke log, dan biasanya tidak ada pemulihan lokal yang bermakna — yang paling masuk akal adalah melempar ke atas dan menyerahkan keputusan kepada lapisan yang lebih tahu konteksnya.

Dua Gaya pada Masalah yang Sama #

Mari kita buktikan klaim itu dengan satu program: menjumlahkan angka dari lima teks, dua di antaranya rusak. Lapisan deteksi kegagalannya satu — std::from_chars yang melaporkan lewat std::errc (keluarga error_code dari Chapter error_code) — tetapi lapisan pelaporannya dibuat dua: satu function melempar, satu function membawa galat sebagai std::expected:

cpp
#include <charconv>
#include <expected>
#include <iostream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <vector>

// Lapisan deteksi kegagalan: std::from_chars melaporkan lewat std::errc
// (keluarga error_code) — netral terhadap gaya pelaporan di lapisan atas.
bool parseAngka(std::string_view teks, int& keluaran, std::errc& galat) {
    if (teks.empty()) {
        galat = std::errc::invalid_argument;
        return false;
    }
    auto [ptr, ec] = std::from_chars(teks.begin(), teks.end(), keluaran);
    if (ec != std::errc()) {              // errc{} = "sukses"
        galat = ec;
        return false;
    }
    if (ptr != teks.end()) {              // "25x" — terurai sebagian
        galat = std::errc::invalid_argument;
        return false;
    }
    return true;
}

// Gaya A: kegagalan luar biasa dilempar sebagai exception.
int konversiLempar(std::string_view teks) {
    int nilai{};
    std::errc ec{};
    if (!parseAngka(teks, nilai, ec)) {
        throw std::invalid_argument("bukan angka valid: " + std::string(teks));
    }
    return nilai;
}

// Gaya B: kegagalan yang diharapkan dibawa sebagai nilai kembalian.
std::expected<int, std::errc> konversiBawa(std::string_view teks) {
    int nilai{};
    std::errc ec{};
    if (!parseAngka(teks, nilai, ec)) {
        return std::unexpected(ec);
    }
    return nilai;
}

const char* namaErrc(std::errc ec) {
    switch (ec) {
        case std::errc::invalid_argument:
            return "invalid_argument";
        case std::errc::result_out_of_range:
            return "result_out_of_range";
        default:
            return "errc lain";
    }
}

int main() {
    const std::vector<std::string> masukan = {
        "120", "x7", "-40", "0", "99999999999999999999"};

    // Gaya A: try/catch mengelilingi pemanggilan.
    int totalA = 0;
    int gagalA = 0;
    for (const std::string& teks : masukan) {
        try {
            totalA += konversiLempar(teks);
        } catch (const std::invalid_argument& e) {
            ++gagalA;
            std::cout << "[exception] gagal: " << e.what() << "\n";
        }
    }

    // Gaya B: satu if untuk setiap hasil.
    int totalB = 0;
    int gagalB = 0;
    for (const std::string& teks : masukan) {
        auto hasil = konversiBawa(teks);
        if (hasil) {
            totalB += hasil.value();
        } else {
            ++gagalB;
            std::cout << "[expected]  gagal: " << namaErrc(hasil.error())
                      << "\n";
        }
    }

    std::cout << "gaya exception: total = " << totalA
              << ", gagal = " << gagalA << "\n";
    std::cout << "gaya expected  : total = " << totalB
              << ", gagal = " << gagalB << "\n";
    return 0;
}

Output yang Dihasilkan:

text
[exception] gagal: bukan angka valid: x7
[exception] gagal: bukan angka valid: 99999999999999999999
[expected]  gagal: invalid_argument
[expected]  gagal: result_out_of_range
gaya exception: total = 80, gagal = 2
gaya expected  : total = 80, gagal = 2

Bedah baris-per-barisnya:

  1. Baris 12-27 (parseAngka): lapisan deteksi dengan konvensi paling tua keluarga error code — bool sebagai penanda sukses plus out-param std::errc. Baris 17 memanggil std::from_chars, baris 18 memeriksa ec != std::errc() (nilai nol berarti sukses), dan baris 22 menangkap teks yang hanya terurai sebagian seperti "25x".
  2. Baris 30 (int konversiLempar(...)): signature gaya A tidak mengumumkan kegagalan apa pun — pembaca hanya melihat int. Keberadaan jalur gagal baru ketahuan saat membaca badan function, tepatnya baris 34 yang melempar std::invalid_argument berisi pesan teks.
  3. Baris 40 (std::expected<int, std::errc> konversiBawa(...)): signature gaya B mendaftar kedua kemungkinan secara eksplisit, dan baris 44 mengembalikannya lewat std::unexpected(ec).
  4. Baris 67-74 (loop gaya A): setiap pemanggilan dibungkus try, dan catch menangkap lewat const&. Laporannya berupa e.what() — teks bebas yang siap ditampilkan, tetapi tidak bisa dibandingkan program.
  5. Baris 79-88 (loop gaya B): satu if per hasil. Laporannya bertipe: hasil.error() adalah std::errc yang bisa dicabangkan lewat namaErrc atau dibandingkan langsung.
  6. Dua baris terakhir output: hasil kedua gaya identik — total 80, gagal 2. Gaya pelaporan bukan soal benar atau salah; yang berbeda adalah struktur alur dan maknanya bagi pembaca.
  7. Perbedaan yang tidak terlihat di output: gaya A melaporkan kedua kegagalan dengan tipe exception yang sama, sehingga bedanya hanya pada teks pesan — kecuali kita mendefinisikan tipe exception tambahan. Gaya B mewarisi pembedaan invalid_argument versus result_out_of_range langsung dari lapisan deteksi.

Biaya: Happy Path Hampir Gratis, Throw Mahal #

Model exception yang dipakai GCC dan Clang sering disebut zero-cost: compiler menyiapkan tabel unwind saat kompilasi, sehingga jalur yang tidak melempar — happy path — berjalan tanpa biaya per-pemanggilan. Bukan benar-benar gratis: ukuran program membesar karena tabel itu, dan optimizer sedikit terikat di titik yang bisa melempar. Tetapi tidak ada biaya berjalan yang menyertai setiap pemanggilan, dan itu yang membuat exception nyaman dipakai untuk kejadian yang jarang.

Biayanya berpindah ke saat throw. Melempar berarti menyusuri stack ke belakang, mencari handler yang cocok lewat tabel unwind, dan menjalankan destructor setiap objek lokal di sepanjang jalan (stack unwinding) sebelum handler dieksekusi. Dibanding mengembalikan int, urutannya berlipat-lipat lebih mahal — angka pastinya bergantung platform dan kedalaman tumpukan, tetapi kesimpulan kualitatifnya stabil: jarang, tidak terasa; sering, mendominasi profil. Sebaliknya jalur nilai — expected atau error_code — biayanya kecil dan konstan di kedua keadaan, karena kegagalannya hanyalah nilai yang dikembalikan seperti nilai lainnya.

Matriks Keputusan #

Kerangka lengkapnya bisa dirangkum dalam satu tabel:

AspekExceptionerror_code / expected
Jenis kegagalanluar biasa — semestinya tidak terjadidiharapkan — bagian pemakaian normal
Frekuensi wajarjarangboleh sering, bahkan ribuan kali per menit
Biaya saat sukseshampir nol (model zero-cost)kecil dan konstan
Biaya saat gagalmahal (stack unwinding)kecil (satu if)
Terlihat di signaturetidak — int baca() bisa saja melemparya — std::expected<int, E> mengumumkannya
Bila diabaikantidak bisa — tak ditangkap berujung terminatebisa diam-diam — butuh disiplin mengecek
Jarak penangananboleh berlapis jauh di atassebaiknya dekat titik kejadian

Beberapa faktor pelengkap yang sering menentukan di dunia nyata:

  • Konsistensi codebase di atas segalanya. Ikuti konvensi yang sudah berlaku: sebagian besar codebase aplikasi menjadikan exception jalur utamanya, dan berpindah gaya di tengah jalan lebih merugikan daripada pilihan yang kurang ideal di satu function.
  • Ada domain yang menonaktifkan exception. Pustaka embedded, game engine, dan sebagian proyek besar mengompilasi dengan flag -fno-exceptions GCC — di sana pelaporan lewat nilai bukan pilihan gaya, melainkan satu-satunya jalan.
  • Terjemahkan di batas modul. Bila bagian dalam modul melempar dan modul lain yang memakainya menolak exception, sisipkan lapisan penerjemahan di boundary: tangkap, lalu laporkan ulang sebagai expected atau error_code.
  • Satu gaya per lapisan. Jangan mencampur keduanya secara acak dalam satu lapisan kode; pembaca function akan sulit memperkirakan ke mana kegagalan mengalir.

Penutup Section: Error Handling #

Section ini berangkat dari mengenali jenis kesalahan pada Chapter Compile-time vs Runtime Errors, lalu membangun mekanisme exception dari hulu ke hilir: Exception dan try-catch untuk alur lempar-tangkap, Throwing Exceptions untuk rincian semantiknya, Standard Exceptions dan Custom Exceptions untuk memilih serta membuat tipenya, Exception Safety Guarantees dan noexcept untuk jaminan yang bisa diberikan, kemudian error_code dan std::expected sebagai dua jalur pelaporan tanpa exception. Chapter ini menutupnya dengan kerangka pemilihan. Benang merahnya satu: kegagalan harus dilaporkan secara eksplisit, tidak boleh ditelan diam-diam, dan alatnya dipilih sesuai makna kegagalan — bukan sekadar kebiasaan.

C++ modern tidak berhenti di sini. Perhatikan bahwa std::expected<int, Galat> yang kita pakai sepanjang dua chapter terakhir adalah class template — satu definisi yang bekerja untuk pasangan tipe apa pun, tanpa menyalin kode secara manual. Menulis tipe dan function seumum itu sendiri adalah topik section berikutnya: Generic Programming dan Templates, dimulai dari Function Templates.

Referensi: cppreference — Exceptions , cppreference — std::exception , GCC Online Docs — C++ Dialect Options (-fno-exceptions) .