Pemilihan strategi error

Exception versus expected versus kode status dan trade-off-nya.

Daftar isi

Exception, std::expected, dan kode status memecahkan masalah yang sama, yaitu menyampaikan kegagalan dari titik kejadiannya ke titik penanganannya, tetapi dengan trade-off yang berbeda pada visibilitas, biaya, dan kebiasaan pemakaiannya. Topik ini membandingkan ketiganya secara berdampingan, memperlihatkan bentuk kode status yang paling tua, lalu menutupnya dengan pedoman pemilihan yang dapat digunakan saat merancang antarmuka fungsi.

Tiga strategi dalam satu perbandingan#

Exception melaporkan kegagalan di luar tipe balik: fungsi tetap mengembalikan nilai hasilnya, sedangkan error menyeberangi beberapa lapisan pemanggil secara otomatis hingga penangan terdekat. std::expected dan kode status melaporkan kegagalan lewat nilai yang dikembalikan, sehingga bentuk kegagalannya terlihat pada deklarasi fungsi, meskipun penerusannya antar lapisan dilakukan secara manual. Perbedaan mendasar ini menentukan sifat-sifat lain yang dirangkum pada tabel berikut.

AspekExceptionstd::expectedKode status
Terlihat pada deklarasi fungsitidakya, lewat tipe balikya, lewat tipe balik atau parameter
Perlakuan bila lupa diperiksaprogram berhenti bila tidak ada penangankegagalan terlewat diam-diamkegagalan terlewat diam-diam
Biaya ketika berhasilrendah, tidak ada pekerjaan tambahanrendahrendah
Biaya ketika gagaltinggi, stack unwinding berjalanrendahrendah
Penerusan antar lapisan fungsiotomatismanual di setiap lapisanmanual di setiap lapisan
Situasi yang cocokkegagalan jarang yang tidak dapat diselesaikan lokalkegagalan yang dapat diduga pada batas modulantarmuka C dan pemeriksaan sederhana

Tabel tersebut memperlihatkan bahwa tidak ada strategi yang unggul pada semua aspek. Exception paling ringan pada jalur berhasil tetapi paling mahal ketika kegagalan benar-benar terjadi, sedangkan dua strategi berbasis nilai menjaga biaya kegagalannya tetap rendah dengan memindahkan beban pemeriksaan kepada pemanggil. Penimbangannya bertumpu pada frekuensi kegagalan, kedalaman lapisan pemanggil, dan disiplin tim yang memakai antarmuka tersebut, bukan pada selera penulisan.

Kode status sebagai pembanding#

Kode status adalah strategi tertua yang tetap dipakai secara luas, terutama pada antarmuka pustaka berbahasa C. Bentuknya memisahkan hasil dan status: fungsi mengembalikan kode status, sedangkan hasilnya dituliskan ke parameter referensi. Program berikut menerapkan bentuk tersebut pada pembagian aman yang sama dengan contoh std::expected.

cpp pembagian-status.cpp
#include <iostream>

// kode status: hasil dan status dikembalikan secara terpisah
enum class Status {
    Berhasil,
    PembagiNol
};

Status bagi(double pembilang, double pembagi, double& hasil) {
    if (pembagi == 0.0) {
        return Status::PembagiNol;
    }
    hasil = pembilang / pembagi;
    return Status::Berhasil;
}

int main() {
    double hasil = 0.0;

    Status s1 = bagi(90, 6, hasil);
    if (s1 == Status::Berhasil) {
        std::cout << "90 / 6 = " << hasil << "\n";
    }

    Status s2 = bagi(90, 0, hasil);
    if (s2 == Status::PembagiNol) {
        std::cout << "90 / 0 ditolak: pembagi bernilai nol\n";
    }
    return 0;
}
text Keluaran
90 / 6 = 15
90 / 0 ditolak: pembagi bernilai nol

Bentuk ini sederhana dan murah, tetapi kelemahannya muncul dari sisi disiplin. Status yang dikembalikan dapat diabaikan tanpa peringatan apa pun dari compiler, sehingga kegagalan berjalan diam-diam hingga akibatnya terasa di tempat lain. Hasil yang ditulis ke parameter referensi juga membawa nilai yang belum tentu sah, sehingga pemanggil yang ceroboh dapat membacanya tanpa memeriksa status terlebih dahulu. Karena itu kode status paling tepat digunakan pada batasan yang memang menuntutnya, misalnya antarmuka biner dengan bahasa C, bukan sebagai pilihan bawaan untuk kode C++ baru.

Trade-off dan pedoman pemilihan#

Pemilihan strategi dimulai dari sifat kegagalannya. Kegagalan yang jarang terjadi, sulit ditangani di tempat kejadiannya, dan sebaiknya diselesaikan di lapisan atas cocok dengan exception, karena penerusannya otomatis dan jalur berhasilnya tetap bersih. Kegagalan yang dapat diduga, misalnya masukan pengguna yang salah atau konfigurasi yang belum diisi, cocok dengan std::expected, karena bentuk kegagalannya eksplisit dan pemanggil memang diharapkan menanganinya segera.

Konsistensi di dalam satu proyek lebih penting daripada pilihan ideal secara teori. Proyek yang menggunakan exception di satu modul dan std::expected di modul lain membutuhkan lapisan konversi pada batas di antara keduanya, dan lapisan tersebut harus ditulis dengan sadar agar tidak ada kegagalan yang hilang di tengahnya. Pedoman yang lazim dirangkum sebagai berikut.

  1. Gunakan exception untuk kondisi luar biasa yang tidak dapat ditangani di tempat kejadiannya, dan tangkap pada lapisan yang mampu memulihkannya.
  2. Gunakan std::expected untuk kegagalan yang merupakan bagian normal dari alur kerja fungsi dan akan ditangani pemanggilnya segera.
  3. Gunakan kode status untuk antarmuka dengan bahasa C, pustaka yang dikompilasi tanpa dukungan exception, dan pemeriksaan sederhana satu lapis.

Konversi antarstrategi dilakukan pada satu titik yang ditetapkan, dengan alasan yang jelas. Nilai std::expected yang gagal dapat diubah menjadi exception dengan melempar std::runtime_error berisi pesan errornya, sedangkan pemanggilan fungsi yang melempar dapat dibungkus try dan catch lalu diubah menjadi nilai std::expected. Satu catatan praktis menutup topik ini: kode yang dikompilasi dengan opsi -fno-exceptions tidak dapat melempar exception sama sekali, sehingga pustaka yang ditargetkan pada lingkungan tersebut wajib menyediakan jalur berbasis nilai atau kode status.

Kesalahan umum#

Kesalahan berikut paling sering muncul ketika strategi error mulai dipilih dan dipadukan.

  • Menggunakan exception untuk alur normal, misalnya memeriksa kunci konfigurasi dengan melempar exception setiap kali kunci tidak ditemukan. Jalur yang sering dilalui menjadi mahal dan tujuan fungsinya tersamarkan. Perbaikannya dengan memindahkan kondisi tersebut ke nilai balik seperti std::optional.
  • Menggabungkan beberapa strategi di satu fungsi, misalnya fungsi yang kadang melempar exception dan kadang mengembalikan kode status. Pemanggil tidak dapat mengandalkan satu kebijakan yang konsisten, sehingga salah satu jalurnya pasti terlewat. Perbaikannya dengan memilih satu strategi per antarmuka dan berpegang padanya.
  • Menelan kegagalan pada lapisan konversi antarstrategi, misalnya bungkus try dan catch yang mengabaikan pesan errornya. Jejak penyebabnya hilang tepat di batas modul. Perbaikannya dengan meneruskan pesan atau konteksnya bersama nilai balik yang baru.

Ringkasan#

  • Exception menyeberangi lapisan pemanggil secara otomatis dengan biaya rendah pada jalur berhasil dan biaya tinggi saat kegagalan, sedangkan std::expected dan kode status memindahkan pemeriksaan kepada pemanggil.
  • Kode status mudah diabaikan karena kegagalannya tidak melekat pada nilai hasil, sehingga penerapannya paling tepat pada batasan antarmuka C dan lingkungan tanpa exception.
  • Konsistensi strategi di dalam satu proyek beserta lapisan konversi yang sadar di antara strategi yang berbeda lebih menentukan daripada pemilihan satu strategi yang ideal.

Referensi#