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:
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:
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:
100 / 4 : Ok(25.0)
100 / 0 : Err("Pembagi tidak boleh nol")
22 / 7 : Ok(3.142857142857143)Mari kita bedah kode di atas:
- 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. - 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. - Baris 5 (
Ok(numerator / denominator)): jalur sukses membungkus hasil pembagian ke dalamOksebagai nilai kembalian baris terakhir fungsi. - Baris 9-11 (tiga
println!dengan format{:?}): format{:?}menampilkan isiResultsecara mentah — terlihatOk(25.0)danErr("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:
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:
Input '34' terbaca sebagai usia 34
Input 'abc' tidak bisa diproses: invalid digit found in string
Berisi sukses? true
Berisi error? falseMari kita telaah kode di atas:
- Baris 1 (
fn parse_age(input: &str) -> Result<u32, std::num::ParseIntError>): parameter dipinjam sebagai&strkarena fungsi hanya membaca teks. Tipe kegagalannya adalahstd::num::ParseIntError— struct bawaan yang menjelaskan kegagalan parsing, dan strukturnya sudah mengimplementasikanDisplaysehingga bisa dicetak dengan{}. - Baris 2 (
input.parse::<u32>()): turbofish::<u32>menegaskan tipe tujuan parsing. MethodparsemengembalikanResultyang mengalir langsung keluar sebagai nilai kembalian fungsi — tidak adaunwrap, tidak adamatch; pemanggil fungsilah yang bertugas menangani. - Baris 6-10 (
match parse_age(input)): pencocokan pola penuh atasResult. CabangOk(age)membongkar nilai sukses, cabangErr(error)membongkar data kegagalan. Kompilator menolakmatchyang lupa salah satu cabang, jadi kedua kemungkinan pasti tertangani. - Baris 13-15 (
is_ok()danis_err()): dua method pemeriksa cepat yang mengembalikanbool, sepupuis_some()/is_none()milikOption. 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.