Error Propagation — Rust

Error Propagation di Rust Ketika fungsi bisa gagal, kita perlu cara untuk meneruskan error ke caller. Rust menyediakan operator ? yang membuat error…

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) -> u32 berisi let porsi = isian.trim().parse::<u32>()?; lalu porsi. 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 })

? 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 di main".

? 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

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 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

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.

Yang akan kamu pelajari