panic menghentikan program; sekarang kita butuh mekanisme untuk kegagalan yang justru harus diteruskan. Misalnya fungsi yang membaca teks "36" dan "abc" dari kolom usia: kedua teks tersebut sama-sama masuk, tetapi hanya satu yang bisa menjadi angka. Fungsi tersebut tidak boleh mati saat menerima "abc" — fungsi harus melaporkan kegagalan itu ke pemanggil dan membiarkan program berjalan. Rust menyelesaikan kebutuhan ini dengan enum Result: nilai kembalian yang memuat dua kemungkinan sekaligus, sukses di satu variant dan kegagalan di variant lainnya. Pada chapter ini kita akan membangun Result buatan sendiri, lalu memakai versi pustaka standar pada kasus parsing yang paling sering kita jumpai.

Result Adalah Enum dengan Dua Variant #

Sama seperti Option<T> yang kita kenal di section Struktur Data, Result adalah enum biasa milik pustaka standar — bukan fitur bahasa khusus. Definisinya kurang lebih seperti ini:

rust
enum Result<T, E> {
    Ok(T),
    Err(E),
}

Dua type parameter mengisi dua variant: T adalah tipe nilai sukses yang dibawa Ok(T), dan E adalah tipe kegagalan yang dibawa Err(E). Sebuah Result<u32, ParseIntError> berarti “mungkin ada u32 bila berhasil, atau ParseIntError bila gagal”. Perbedaan mendasar dari Option terletak pada variant kedua: None hanya menyatakan ketiadaan, sedangkan Err(E) menyimpan data kegagalan — jenis kesalahan, pesan, atau konteks apa pun yang kita pilih. Karena Result dipakai sangat sering, variant Ok dan Err dapat ditulis tanpa awalan Result::.

Kompilator memperlakukan Result seperti enum lain: mencocokkan polanya lewat match menuntut kedua cabang ditangani, dan mengabaikan hasil Result sepenuhnya memicu peringatan. Keberhasilan dan kegagalan kini menjadi bagian dari tipe, sehingga pemanggil tidak bisa lupa bahwa sebuah operasi bisa gagal.

Membangun Result Buatan Sendiri #

Sebelum memakai versi pustaka standar, kita buat dulu Result secara manual agar mekanismenya terlihat utuh. Fungsi pembagi berikut mengembalikan Result<f64, String> — sukses berupa f64, kegagalan berupa String berisi penjelasan:

rust
fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
    if denominator == 0.0 {
        return Err(String::from("Pembagi tidak boleh nol"));
    }
    Ok(numerator / denominator)
}

fn main() {
    println!("100 / 4 : {:?}", divide(100.0, 4.0));
    println!("100 / 0 : {:?}", divide(100.0, 0.0));
    println!("22 / 7  : {:?}", divide(22.0, 7.0));
}

Output yang Dihasilkan:

text
100 / 4 : Ok(25.0)
100 / 0 : Err("Pembagi tidak boleh nol")
22 / 7  : Ok(3.142857142857143)

Mari kita bedah kode di atas:

  1. Baris 1 (fn divide(numerator: f64, denominator: f64) -> Result<f64, String>): tanda tangan menjanjikan dua kemungkinan sejak awal — Ok(f64) bila pembagian sah, Err(String) bila pembagi nol. Pemanggil melihat janji ini sebelum memanggil.
  2. Baris 2-4 (if denominator == 0.0 { return Err(...) }): pengecekan kegagalan berdiri paling depan. Err(String::from("Pembagi tidak boleh nol")) membungkus pesan kegagalan ke dalam variant — fungsi pulang membawa laporan, bukan menghentikan program.
  3. Baris 5 (Ok(numerator / denominator)): jalur sukses membungkus hasil pembagian ke dalam Ok sebagai nilai kembalian baris terakhir fungsi.
  4. Baris 9-11 (tiga println! dengan format {:?}): format {:?} menampilkan isi Result secara mentah — terlihat Ok(25.0) dan Err("Pembagi tidak boleh nol") apa adanya. Cara ini pas untuk inspeksi cepat, tetapi bukan pola penanganan produksi; pola yang tepat kita bahas beberapa paragraf di bawah.

Pembagian dengan nol pada f64 sebenarnya menghasilkan NaN, bukan panic — namun membiarkan NaN merembet ke perhitungan berikutnya jauh lebih berbahaya daripada menghentikannya di pintu masuk. Pola Err di fungsi divide memilih jalur ketiga: gagal terkendali, dengan pesan yang bisa dibaca pemanggil.

Parsing Angka, Kasus Paling Umum #

Operasi yang paling sering menghasilkan Result di kode sehari-hari adalah parsing teks menjadi angka. Method parse pada str mencoba mengubah teks menjadi tipe numerik yang kita tentukan; teks "36" berhasil menjadi 36, sedangkan teks "abc" gagal dan menghasilkan Err(ParseIntError). Fungsi berikut membungkus pemanggilan parse untuk kolom usia:

rust
fn parse_age(input: &str) -> Result<u32, std::num::ParseIntError> {
    input.parse::<u32>()
}

fn main() {
    for input in ["34", "abc"] {
        match parse_age(input) {
            Ok(age) => println!("Input '{}' terbaca sebagai usia {}", input, age),
            Err(error) => println!("Input '{}' tidak bisa diproses: {}", input, error),
        }
    }

    let hasil = parse_age("29");
    println!("Berisi sukses? {}", hasil.is_ok());
    println!("Berisi error? {}", hasil.is_err());
}

Output yang Dihasilkan:

text
Input '34' terbaca sebagai usia 34
Input 'abc' tidak bisa diproses: invalid digit found in string
Berisi sukses? true
Berisi error? false

Mari kita telaah kode di atas:

  1. Baris 1 (fn parse_age(input: &str) -> Result<u32, std::num::ParseIntError>): parameter dipinjam sebagai &str karena fungsi hanya membaca teks. Tipe kegagalannya adalah std::num::ParseIntError — struct bawaan yang menjelaskan kegagalan parsing, dan strukturnya sudah mengimplementasikan Display sehingga bisa dicetak dengan {}.
  2. Baris 2 (input.parse::<u32>()): turbofish ::<u32> menegaskan tipe tujuan parsing. Method parse mengembalikan Result yang mengalir langsung keluar sebagai nilai kembalian fungsi — tidak ada unwrap, tidak ada match; pemanggil fungsilah yang bertugas menangani.
  3. Baris 6-10 (match parse_age(input)): pencocokan pola penuh atas Result. Cabang Ok(age) membongkar nilai sukses, cabang Err(error) membongkar data kegagalan. Kompilator menolak match yang lupa salah satu cabang, jadi kedua kemungkinan pasti tertangani.
  4. Baris 13-15 (is_ok() dan is_err()): dua method pemeriksa cepat yang mengembalikan bool, sepupu is_some()/is_none() milik Option. Method ini cocok untuk keputusan ya/tidak tanpa perlu membongkar isinya.

Isi ParseIntError tampil sebagai invalid digit found in string — pesan yang menjelaskan bahwa teks memuat karakter yang bukan digit. Pesan tersebut berasal dari implementasi Display milik ParseIntError, bukan teks yang kita tulis sendiri; kita hanya menyampaikannya.

Dua Jalur, Satu Tipe #

Membaca kedua contoh di atas, pola kerjanya konsisten: fungsi memeriksa kondisi gagal lebih dulu dan pulang membawa Err, atau melanjutkan pekerjaan dan pulang membawa Ok. Pemanggil mengambil keputusan melalui match, dan program tetap hidup di kedua jalur. Perhatikan juga bahwa Result tidak membawa informasi “kenapa program harus mati” — sebab Result memang tidak dimaksudkan untuk mematikan apa pun; Result hanya mengalihkan tanggung jawab keputusan dari fungsi ke pemanggilnya.

Aliran data ini berbeda total dari panic: panic memutus aliran eksekusi di titik kejadian, sedangkan Result mengalir sebagai nilai melalui pemanggilan fungsi, bisa disimpan di variabel (let hasil = ...), diperiksa (is_err()), dan diputuskan kemudian. Konsekuensinya, tanda tangan fungsi menjadi kontrak yang jujur — pembaca kode tahu persis operasi mana yang bisa gagal tanpa membaca implementasinya.

Ringkasan #

Result<T, E> adalah enum dua variant — Ok(T) membawa sukses, Err(E) membawa kegagalan — dan kompilator memaksa kedua cabangnya ditangani saat pencocokan pola. Fungsi divide menunjukkan pembuatan Result manual dengan tipe kegagalan String, sedangkan parse menunjukkan pemakaian ParseIntError bawaan lengkap dengan method pemeriksa is_ok()/is_err(). Menangani Result lewat match selalu valid, tetapi menulis match penuh untuk setiap pemanggilan parsing membuat kode cepat gemuk. Rust menyediakan beberapa pintasan — mulai dari unwrap dan expect pada chapter berikutnya — dengan karakter dan risiko yang perlu kita pahami sebelum memakainya.