Error propagation artinya meneruskan error ke pemanggil, alih-alih menanganinya di tempat. Rust punya operator kecil untuk itu: ?. Bayangin kasir magang di Kedai Martabak Bang Rusdi. Tiap pesanan ia kerjakan langkah demi langkah: cek stok telur, baca uang yang diterima, hitung kembalian. Kalau satu langkah bermasalah, ia gak mengarang solusi sendiri: ia berhenti dan langsung menyerahkan masalahnya ke Bang Rusdi, lengkap dengan catatan apa yang terjadi. Kalau semuanya aman, ia lanjut ke langkah berikutnya. Kebiasaan "aman? lanjut. Ada masalah? serahkan ke atasan sekarang juga" itu operator ?, dan Bang Rusdi itu fungsi pemanggilnya.
🧪 Tebakan cepat:
fn baca_porsi(isian: &str) -> u32berisilet porsi = isian.trim().parse::<u32>()?;laluporsi. Lolos compile? Tebak dulu, jawabannya ada di bagian "? cuma boleh di fungsi yang mengembalikan Result atau Option".
Operator ?: lanjut, atau kembalikan error-nya
use std::num::ParseIntError;
fn baca_porsi(isian: &str) -> Result<u32, ParseIntError> {
let porsi = isian.trim().parse::<u32>()?;
Ok(porsi)
}
fn total(harga: u32, isian: &str) -> Result<u32, ParseIntError> {
let porsi = baca_porsi(isian)?;
println!("porsi terbaca: {}", porsi);
Ok(harga * porsi)
}
fn main() {
println!("{:?}", total(32000, " 2 "));
println!("{:?}", total(32000, "dua"));
}
porsi terbaca: 2
Ok(64000)
Err(ParseIntError { kind: InvalidDigit })
?ditaruh di belakang sebuahResult. Kalau isinyaOk(nilai), ekspresinya jadinilaidan fungsi jalan terus. KalauErr(e), fungsi itu langsung selesai dan mengembalikanErr(e)ke pemanggilnya.- Untuk
"dua",baca_porsimengembalikanErrketotal, dan?ditotalmeneruskannya kemain. Barisporsi terbacagak pernah jalan: begitu ada error, sisa fungsi dilewati. - Fungsinya sendiri harus mengembalikan
Result, dan nilai suksesnya dibungkusOk(...). LupaOk,porsisaja di baris terakhir, ditolak denganexpected `Result<u32, ParseIntError>`, found `u32`.
? cuma singkatan. Tanpa ?, baca_porsi ditulis begini, dan hasilnya sama persis:
fn baca_porsi(isian: &str) -> Result<u32, ParseIntError> {
let porsi = match isian.trim().parse::<u32>() {
Ok(p) => p,
Err(e) => return Err(e),
};
Ok(porsi)
}
Hal yang sama berlaku untuk membaca file: let isi = std::fs::read_to_string(path)?; memberi isi file-nya, atau mengembalikan std::io::Error ke pemanggil.
? cuma boleh di fungsi yang mengembalikan Result atau Option
Ini kode tebakan tadi:
fn baca_porsi(isian: &str) -> u32 {
let porsi = isian.trim().parse::<u32>()?;
porsi
}
fn main() {
println!("{}", baca_porsi("2"));
}
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:2:44
|
1 | fn baca_porsi(isian: &str) -> u32 {
| --------------------------------- this function should return `Result` or `Option` to accept `?`
2 | let porsi = isian.trim().parse::<u32>()?;
| ^ cannot use the `?` operator in a function that returns `u32`
Gak lolos. Itu jawaban tebakannya. Kalau parse-nya gagal, ? harus mengembalikan error, padahal fungsi ini cuma bisa mengembalikan u32. Perbaikannya: ubah tipe kembaliannya jadi Result<u32, ParseIntError> dan kembalikan Ok(porsi). main biasa juga gak bisa memakai ?, karena ia mengembalikan (); caranya ada di bagian "Box
? pada Option
Di fungsi yang mengembalikan Option, ? meneruskan None. Dengan harga dari pelajaran sebelumnya:
fn harga_dua(a: &str, b: &str) -> Option<u32> {
Some(harga(a)? + harga(b)?)
}
harga_dua("martabak manis", "es teh") memberi Some(37000), dan harga_dua("martabak manis", "kopi") memberi None. Tapi ? gak mencampur keduanya: harga(menu)? di fungsi yang mengembalikan Result<u32, String> ditolak dengan the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result` , dan compiler menyarankan use `.ok_or(...)?` . Beri None sebuah alasan dulu:
fn total(menu: &str, porsi: u32) -> Result<u32, String> {
let h = harga(menu).ok_or(format!("{} gak ada di menu", menu))?;
Ok(h * porsi)
}
total("martabak telur", 2) memberi Ok(76000), total("kopi", 2) memberi Err("kopi gak ada di menu").
Error buatan sendiri: KedaiError
String sebagai error cuma bisa dibaca manusia. Pemanggil yang ingin bereaksi beda untuk tiap masalah butuh enum:
use std::fmt;
#[derive(Debug)]
enum KedaiError {
StokHabis,
UangKurang(u32),
}
impl fmt::Display for KedaiError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
KedaiError::StokHabis => write!(f, "Stok telur habis"),
KedaiError::UangKurang(kurang) => write!(f, "Uang kurang {}", kurang),
}
}
}
impl std::error::Error for KedaiError {}
fn cek_stok(stok_telur: u8, butuh: u8) -> Result<(), KedaiError> {
if stok_telur < butuh {
return Err(KedaiError::StokHabis);
}
Ok(())
}
fn bayar(total: u32, uang: u32) -> Result<u32, KedaiError> {
if uang < total {
return Err(KedaiError::UangKurang(total - uang));
}
Ok(uang - total)
}
fn pesan(stok_telur: u8, uang: u32) -> Result<u32, KedaiError> {
cek_stok(stok_telur, 3)?;
let kembalian = bayar(102000, uang)?;
Ok(kembalian)
}
fn main() {
println!("{:?}", pesan(250, 110000));
println!("{:?}", pesan(250, 100000));
println!("{:?}", pesan(2, 110000));
match pesan(250, 100000) {
Ok(k) => println!("Kembalian {}", k),
Err(e) => println!("Gagal: {}", e),
}
}
Ok(8000)
Err(UangKurang(2000))
Err(StokHabis)
Gagal: Uang kurang 2000
- Tiap variant satu jenis masalah.
UangKurang(u32)membawa kekurangannya, persis sepertiPembayaran::Tunaidi pelajaran enum: total 102000, uang 100000, kurang 2000. #[derive(Debug)]memberi bentuk{:?}(UangKurang(2000)), danimpl fmt::Displaymemberi kalimat untuk pembeli (Uang kurang 2000).impl std::error::Error for KedaiError {}menjadikannya error "resmi". Isinya kosong karena semua methodErrorpunya default; syaratnya cumaDebugdanDisplay. Berkat iniKedaiErrorbisa masukBox<dyn Error>.Result<(), KedaiError>artinya "berhasil tanpa hasil apa-apa, atau gagal".()dibaca unit: nilai kosong.pesanmemakai?dua kali. Stok 2 berhenti dicek_stok, danbayargak pernah dipanggil.
From: supaya ? bisa mengubah error
Sekarang kasir membaca uangnya dari teks: let uang = uang_teks.trim().parse::<u32>()?; di dalam pesan. parse memberi ParseIntError, padahal pesan mengembalikan KedaiError, jadi compiler menolak: E0277 `?` couldn't convert the error to `KedaiError`, the trait `From<ParseIntError>` is not implemented for `KedaiError` . Catatan di bawahnya menjelaskan kuncinya: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait. Jadi ? gak cuma meneruskan, ia juga mengubah error-nya lewat From. Tambahkan variant untuknya dan ajari cara mengubahnya:
use std::num::ParseIntError;
#[derive(Debug)]
enum KedaiError {
StokHabis,
UangKurang(u32),
BukanAngka(ParseIntError),
}
// di dalam match pada impl fmt::Display, tambah satu cabang:
// KedaiError::BukanAngka(e) => write!(f, "Uang bukan angka: {}", e),
impl From<ParseIntError> for KedaiError {
fn from(e: ParseIntError) -> Self {
KedaiError::BukanAngka(e)
}
}
fn pesan(stok_telur: u8, uang_teks: &str) -> Result<u32, KedaiError> {
cek_stok(stok_telur, 3)?;
let uang = uang_teks.trim().parse::<u32>()?;
let kembalian = bayar(102000, uang)?;
Ok(kembalian)
}
pesan(250, "110000") memberi Ok(8000), pesan(250, "seratus ribu") memberi Err(BukanAngka(ParseIntError { kind: InvalidDigit })), dan dicetak dengan {} jadi Uang bukan angka: invalid digit found in string. Cara lain tanpa From: .map_err(KedaiError::BukanAngka)? di baris itu saja.
Box di main
main boleh mengembalikan Result. Tipe error yang paling luwes adalah Box<dyn Error>: "error apa pun yang punya trait Error". dyn kamu kenal dari pelajaran traits, dan Box dibahas di pelajaran berikutnya.
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let porsi: u32 = "2".parse()?;
println!("Porsi {}", porsi);
let kembalian = pesan(250, "110000")?;
println!("Kembalian {}", kembalian);
let kembalian = pesan(250, "100000")?;
println!("Kembalian {}", kembalian);
Ok(())
}
Porsi 2
Kembalian 8000
Error: UangKurang(2000)
?di sini mengubahParseIntErrordanKedaiErrorjadiBox<dyn Error>sendiri, karena keduanya punya traitError.- Begitu
mainmengembalikanErr, Rust mencetakError:diikuti bentukDebug-nya (UangKurang(2000), bukanUang kurang 2000), dan program keluar dengan kode 1. Box<dyn Error>praktis untukmaindan program kecil, tapi pemanggil gak bisa lagimatchvariant-nya dengan mudah. Fungsi yang dipakai kode lain sebaiknya mengembalikan enum sepertiKedaiError.
📌 Di dunia nyata: thiserror dan anyhow
Menulis Display dan From dengan tangan itu berulang, jadi hampir semua project Rust memakai dua crate (library dari crates.io, dibahas di pelajaran berikutnya): thiserror (versi 2.0.21 saat ini) untuk mendefinisikan error, dan anyhow (versi 1.0.104) untuk menerima error apa pun di aplikasi. Setelah cargo add thiserror anyhow:
use anyhow::Context;
use thiserror::Error;
#[derive(Debug, Error)]
enum KedaiError {
#[error("Stok telur habis")]
StokHabis,
#[error("Uang kurang {0}")]
UangKurang(u32),
#[error("Uang bukan angka: {0}")]
BukanAngka(#[from] std::num::ParseIntError),
}
// cek_stok, bayar, dan pesan sama persis seperti di atas.
fn main() -> anyhow::Result<()> {
let kembalian = pesan(250, "110000").context("pesanan Budi gagal")?;
println!("Kembalian {}", kembalian);
let kembalian = pesan(250, "100000").context("pesanan Sari gagal")?;
println!("Kembalian {}", kembalian);
Ok(())
}
Kembalian 8000
Error: pesanan Sari gagal
Caused by:
Uang kurang 2000
#[error("...")] menulis Display, dan #[from] menulis From. .context(...) menempelkan keterangan di atas error aslinya, dan anyhow mencetak keduanya. Pegangan umumnya: thiserror untuk library (pemanggilnya perlu match variant), anyhow untuk aplikasi (cukup laporkan dengan jelas).
⚠️ Jebakan yang sering ketemu
?di fungsi yang gak mengembalikanResultatauOption. E0277, termasuk difn main()biasa. Ubah tipe kembaliannya.- Lupa
Ok(...)di akhir.expected `Result<u32, ParseIntError>`, found `u32`. Bungkus nilai suksesnya. ?padaOptiondi fungsi yang mengembalikanResult. E0277. Pakai.ok_or(...)?.- Tipe error beda tanpa
From.`?` couldn't convert the error to `KedaiError`. Tulisimpl From<ParseIntError> for KedaiError, atau.map_err(...). - Mengira
mainmencetakDisplay. Error darimaindicetak denganDebug. Kalau perlu kalimat rapi, tangani denganmatchdanprintln!("{}", e).
TL;DR: ? di belakang Result memberi isi Ok, atau langsung mengembalikan Err ke pemanggil; di belakang Option ia meneruskan None. Ia cuma boleh dipakai di fungsi yang mengembalikan Result atau Option (E0277), dan nilai suksesnya dibungkus Ok(...). Error buatan sendiri adalah enum seperti KedaiError { StokHabis, UangKurang(u32) } dengan #[derive(Debug)], impl fmt::Display, dan impl std::error::Error. ? mengubah error lewat From, jadi impl From<ParseIntError> for KedaiError membuat parse::<u32>()? lolos. fn main() -> Result<(), Box<dyn Error>> menerima error apa pun, mencetaknya dengan Debug, dan keluar dengan kode 1. Di project sungguhan: thiserror (2.0.21) untuk mendefinisikan error, anyhow (1.0.104) untuk aplikasi.