C++ menyediakan beberapa mekanisme penanganan error: exception (throw/try/catch), error code (return-based), std::optional (C++17), std::variant (C++17), dan std::expected (C++23). Pilihan tidak universal — exception baik untuk kasus langka dan fatal, error code cocok untuk failure yang rutin dan expected, optional/variant menangani “tidak ada nilai” tanpa overhead. Pada Chapter ini kita akan membandingkan kelima pendekatan, menjelaskan kapan memilih masing-masing, dan menutup dengan RAII yang menjamin pembersihan resource bahkan saat exception terlempar.

Lima Pendekatan #

PendekatanSintaksCocok untuk
Exceptionthrow, try/catchError langka/fatal; gagal total
Error codereturn std::error_codeFailure rutin; tidak fatal
std::optional<T>optional<T>, nulloptMungkin tidak ada nilai
std::variant<T, E>tagged unionSukses ATAU error dengan info
std::expected<T, E> (C++23)expected<T, E>Modern result type

Mekanisme: Exception dan Stack Unwinding #

Saat throw e dieksekusi, runtime mulai stack unwinding — keluar dari fungsi saat ini, kemudian caller, kemudian caller-of-caller, dst., sampai menemukan catch yang cocok. Setiap scope yang ditinggalkan memanggil destructor objek lokal (RAII). Bila tidak ada catch yang cocok, std::terminate dipanggil → program abort.

Exception mahal saat dilempar (beberapa mikrodetik hingga milidetik) tetapi gratis saat tidak dilempar (zero-cost exception model di mayoritas compiler modern). Karena itu, exception baik untuk failure yang jarang terjadi — happy path tetap cepat.

Error code mahal saat harus diperiksa (branch di setiap call site) tetapi eksplisit. Untuk failure yang rutin (mis. parse gagal, file tidak ditemukan), error code sering lebih jernih.

Kode: Lima Pendekatan Berdampingan #

cpp
#include <iostream>
#include <string>
#include <vector>
#include <stdexcept>
#include <system_error>
#include <variant>
#include <optional>

// 1) Exception-based error handling
double divide_exc(double a, double b) {
    if (b == 0) throw std::runtime_error("pembagi nol");
    return a / b;
}

// 2) Error code (return-based)
std::error_code divide_ec(double a, double b, double& out) {
    if (b == 0) return std::make_error_code(std::errc::invalid_argument);
    out = a / b;
    return {};
}

// 3) std::optional (C++17) — ada/tidak ada nilai
std::optional<double> divide_opt(double a, double b) {
    if (b == 0) return std::nullopt;
    return a / b;
}

// 4) std::variant (C++17) — result type (sukses | error)
struct DivResult {
    std::variant<double, std::string> v;
    bool ok() const { return std::holds_alternative<double>(v); }
    double value() const { return std::get<double>(v); }
    std::string err() const { return std::get<std::string>(v); }
};

DivResult divide_var(double a, double b) {
    if (b == 0) return {std::variant<double, std::string>{std::string{"pembagi nol"}}};
    return {std::variant<double, std::string>{a / b}};
}

// 5) Exception hierarchy
class MathError : public std::runtime_error {
public:
    MathError(const std::string& msg) : std::runtime_error(msg) {}
};
class DivByZero : public MathError {
public:
    DivByZero() : MathError("division by zero") {}
};

double safe_div(double a, double b) {
    if (b == 0) throw DivByZero{};
    return a / b;
}

int main() {
    // 1) Exception
    try {
        double r = divide_exc(10, 0);
        std::cout << "hasil: " << r << '\n';
    } catch (const std::runtime_error& e) {
        std::cout << "exception: " << e.what() << '\n';
    }

    // 2) Error code
    double out;
    auto ec = divide_ec(10, 0, out);
    if (ec) {
        std::cout << "error_code: " << ec.message() << '\n';
    } else {
        std::cout << "hasil: " << out << '\n';
    }

    // 3) std::optional
    auto r3 = divide_opt(10, 0);
    if (r3) {
        std::cout << "hasil: " << *r3 << '\n';
    } else {
        std::cout << "optional: nullopt\n";
    }

    // value_or — default bila kosong
    double dv = divide_opt(10, 0).value_or(-1);
    std::cout << "value_or(-1): " << dv << '\n';

    // 4) std::variant
    auto r4 = divide_var(10, 0);
    if (r4.ok()) std::cout << "variant: " << r4.value() << '\n';
    else        std::cout << "variant err: " << r4.err() << '\n';

    // 5) Exception hierarchy
    try {
        safe_div(5, 0);
    } catch (const DivByZero& e) {
        std::cout << "caught DivByZero: " << e.what() << '\n';
    } catch (const MathError& e) {
        std::cout << "caught MathError: " << e.what() << '\n';
    } catch (const std::exception& e) {
        std::cout << "caught exception: " << e.what() << '\n';
    }

    // 6) RAII dengan exception safety
    std::vector<int> v = {1, 2, 3};
    try {
        v.push_back(4);
        throw std::runtime_error("test");
    } catch (...) {
        std::cout << "vector masih utuh: " << v.size() << " elemen\n";
        // vector tidak bocor — RAII membersihkan otomatis
    }

    return 0;
}

Output yang Dihasilkan:

text
exception: pembagi nol
error_code: Invalid argument
optional: nullopt
value_or(-1): -1
variant err: pembagi nol
caught DivByZero: division by zero
vector masih utuh: 4 elemen

Bedah Kode Baris-per-Baris #

  1. throw std::runtime_error("pembagi nol"): melempar exception. Runtime mulai stack unwinding — keluar dari divide_exc, mencari catch di caller. catch (const std::runtime_error& e) menangkap, mencetak e.what().
  2. std::error_code divide_ec(...): kembalikan error code sebagai return value. Caller harus memeriksa if (ec) — ec truthy bila berisi error. Idiomatik untuk error yang rutin diharapkan.
  3. std::optional<double> divide_opt(...): kembalikan nullopt bila gagal, nilai bila sukses. Caller memeriksa if (r3) lalu dereference *r3. Bersih dan ekspresif.
  4. r3.value_or(-1): idiomatik untuk “nilai default bila kosong”. Hindari pengecekan eksplisit — kode lebih ringkas.
  5. std::variant<double, std::string>: tagged union — menyimpan salah satu dari double atau std::string. std::holds_alternative<double>(v) memeriksa tipe aktif; std::get<double>(v) mengakses nilai.
  6. class MathError : public std::runtime_error: exception hierarchy. Subclass DivByZero memungkinkan catch (const DivByZero&) menangkap specifically, sementara catch (const MathError&) menangkap semua error matematika. Urutan catch dari specific ke general.
  7. catch (...): catch-all — menangkap exception apa pun. Berguna sebagai last-resort cleanup, tetapi tidak memberikan info error.

Exception Safety Guarantees #

Empat level jaminan safety:

  1. No-throw: fungsi tidak melempar (noexcept). Destructor, swap, move — wajib no-throw.
  2. Strong: bila gagal, state program kembali seperti sebelum call. Mis. vector::push_back — bila gagal alokasi, vector tetap utuh.
  3. Basic: bila gagal, program masih valid (tidak corrupt) tetapi state mungkin berubah. Mayoritas STL.
  4. No guarantee: bila gagal, program mungkin corrupt. Hindari di kode modern.

RAII adalah kunci strong guarantee: bila operasi gagal setelah sebagian resource diakuisisi, destructor objek lokal membersihkan secara otomatis. Contoh di kode: walaupun exception dilempar setelah v.push_back(4), vector tetap valid dan utuh.

Kapan Memilih Apa #

SkenarioPilihan
Error langka dan fatal (out of memory, corrupted state)Exception
Failure rutin dan diharapkan (parse error, file not found)Error code atau optional
Mungkin tidak ada nilai (lookup miss)std::optional
Sukses atau error dengan info detailstd::variant atau std::expected (C++23)
API yang harus noexcept (destructor, move, swap)noexcept + return code
Boundary ke code CError code (C tidak paham exception)

std::expected<T, E> (C++23) #

C++23 menambahkan std::expected<T, E> — tipe result yang berisi nilai T atau error E. Lebih ringkas dari variant:

cpp
std::expected<double, std::string> divide(double a, double b) {
    if (b == 0) return std::unexpected("pembagi nol");
    return a / b;
}

auto r = divide(10, 0);
if (r.has_value()) std::cout << *r << '\n';
else                std::cout << r.error() << '\n';

Untuk compiler pra-C++23, pola std::variant<T, E> meniru expected secara manual.

Ringkasan #

  • Lima mekanisme: exception, error code, optional, variant, expected (C++23).
  • Exception gratis saat tidak lempar, mahal saat lempar; cocok untuk failure langka.
  • Error code eksplisit dan cepat, cocok untuk failure rutin.
  • optional<T> bersih untuk “mungkin tidak ada nilai”; variant<T, E> menambah info error.
  • Tangkap exception by const reference (catch (const E& e)), bukan by-value (slicing).
  • Destructor wajib noexcept — jangan lempar dari destructor.
  • RAII menjamin pembersihan resource bahkan saat exception — tulis kode dengan strong/basic guarantee.
  • Untuk DSA kompetitif, optional paling sering cukup. Untuk pustaka produksi, expected (C++23) adalah pilihan modern.