Penanganan Error di Rust dengan anyhow dan thiserror
Di halaman Result dan Error kustom, kamu menulis tipe error secara manual: sebuah enum, sebuah impl Display, dan mungkin impl From supaya ? bisa dipakai. Itu cara yang tepat untuk belajar. Tapi di proyek sungguhan, kamu akan menulis kode boilerplate (kode berulang) yang sama terus-menerus.
Ada dua crate (pustaka Rust) kecil, buatan penulis yang sama, yang menghilangkan boilerplate itu. Keduanya menyelesaikan dua masalah yang berbeda:
thiserrormembantumu mendefinisikan tipe error. Kamu tetap menulis enum-nya, tapi sebuah#[derive]akan menuliskanDisplay,Error, danFromuntukmu.anyhowmembantumu meneruskan error. Crate ini memberimu satu tipe error yang bisa menampung error apa saja, plus cara mudah untuk menambahkan pesan seperti “saat memuat file konfigurasi”.
Library vs aplikasi
Section titled “Library vs aplikasi”Pilihanmu bergantung pada siapa yang menangani error-nya.
Library adalah kode yang dipanggil orang lain. Pemanggilnya mungkin ingin bereaksi berbeda untuk setiap kegagalan: mencoba lagi saat timeout, menampilkan pesan error di formulir saat input salah. Jadi library sebaiknya mengembalikan enum yang presisi, yang bisa di-match oleh pemanggilnya. Itulah thiserror.
Aplikasi (main.rs milikmu) biasanya adalah perhentian terakhir. Sebagian besar error berakhir sebagai pesan untuk manusia, dan mungkin sebuah exit code. Kamu jarang men-match error-nya; kamu ingin error itu mudah diteruskan ke atas dengan ? dan mudah dibaca. Itulah anyhow.
Patokan yang bagus: thiserror untuk kode yang dipanggil orang lain, anyhow untuk lapisan teratas programmu. Banyak proyek memakai keduanya, seperti yang akan kamu lihat di akhir halaman ini.
Persiapan
Section titled “Persiapan”cargo add thiserror anyhow[dependencies]anyhow = "1"thiserror = "2"Baru kenal Cargo.toml? Lihat Cargo dan Crate. Kedua crate ini tersedia di Rust Playground, jadi setiap tombol “Try it Yourself” di halaman ini bisa dipakai.
thiserror: derive Error dan tulis pesannya
Section titled “thiserror: derive Error dan tulis pesannya”Pasang #[derive(Debug, Error)] pada enum-mu, lalu beri setiap varian pesan #[error("...")]. Pesan itu menjadi teks Display untuk varian tersebut. Di dalamnya, {0} berarti field tuple pertama, dan {balance} berarti field bernama:
use thiserror::Error;
#[derive(Debug, Error)]enum BankError { #[error("akun `{0}` tidak ditemukan")] NotFound(String), #[error("saldo tidak cukup: saldo {balance}, mencoba mengambil {amount}")] InsufficientFunds { balance: u32, amount: u32 }, #[error("jumlah harus lebih dari nol")] ZeroAmount,}
fn main() { let errors = vec![ BankError::NotFound(String::from("alice")), BankError::InsufficientFunds { balance: 50, amount: 80 }, BankError::ZeroAmount, ];
for e in &errors { println!("{e}"); } println!("{:?}", errors[1]);}Output:
akun `alice` tidak ditemukansaldo tidak cukup: saldo 50, mencoba mengambil 80jumlah harus lebih dari nolInsufficientFunds { balance: 50, amount: 80 }Ini persis enum dari halaman Error kustom, tapi seluruh blok impl Display sudah hilang. Tidak ada yang disembunyikan saat runtime: thiserror hanya menulis kode yang sebenarnya akan kamu tulis sendiri. Pemanggil tetap melihat enum biasa.
thiserror: #[from] dan #[source]
Section titled “thiserror: #[from] dan #[source]”Error sering punya penyebab: “port tidak valid” karena “invalid digit found in string”. Trait Error standar punya method .source() untuk ini, dan thiserror mengisinya untukmu.
#[from] melakukan dua hal. Ia menandai field itu sebagai source (sumber error), dan ia menulis impl From, sehingga ? otomatis mengubah error di dalamnya menjadi varianmu:
use std::error::Error; // membawa method .source() ke dalam scopeuse std::num::ParseIntError;
#[derive(Debug, thiserror::Error)]enum ConfigError { #[error("tidak ada `=` di baris {0:?}")] NoEquals(String), #[error("port bukan angka yang valid")] BadPort(#[from] ParseIntError),}
fn parse_port(line: &str) -> Result<u16, ConfigError> { let (_, value) = line .split_once('=') .ok_or_else(|| ConfigError::NoEquals(line.to_string()))?; let port = value.trim().parse::<u16>()?; // ParseIntError berubah menjadi ConfigError::BadPort Ok(port)}
fn main() { for line in ["port = 8080", "port 8080", "port = delapan"] { match parse_port(line) { Ok(port) => println!("ok: {port}"), Err(e) => { println!("error: {e}"); if let Some(cause) = e.source() { println!(" disebabkan oleh: {cause}"); } } } }}Output:
ok: 8080error: tidak ada `=` di baris "port 8080"error: port bukan angka yang valid disebabkan oleh: invalid digit found in string“invalid digit found in string” adalah pesan bawaan Rust untuk ParseIntError, jadi tetap dalam bahasa Inggris. Artinya: ada karakter yang bukan digit di dalam string.
Kadang kamu ingin punya source tapi tanpa From otomatis, karena kamu perlu menambahkan data lain, misalnya nomor baris. Kalau begitu, pakai #[source] pada field-nya, dan buat error-nya sendiri dengan .map_err(). (Field yang namanya persis source otomatis dianggap sebagai source.) thiserror juga bisa dipakai pada struct, bukan hanya enum:
use std::error::Error;use std::num::ParseIntError;
#[derive(Debug, thiserror::Error)]#[error("baris {line} tidak berisi angka")]struct LineError { line: usize, #[source] cause: ParseIntError,}
fn sum_lines(text: &str) -> Result<i32, LineError> { let mut total = 0; for (i, row) in text.lines().enumerate() { let n: i32 = row .trim() .parse() .map_err(|cause| LineError { line: i + 1, cause })?; total += n; } Ok(total)}
fn main() { println!("total: {}", sum_lines("10\n20\n30").unwrap());
let err = sum_lines("10\ndua puluh\n30").unwrap_err(); println!("{err}"); println!("disebabkan oleh: {}", err.source().unwrap());}Output:
total: 60baris 2 tidak berisi angkadisebabkan oleh: invalid digit found in stringanyhow: satu tipe Result untuk semuanya
Section titled “anyhow: satu tipe Result untuk semuanya”anyhow::Result<T> adalah singkatan dari Result<T, anyhow::Error>. Sebuah anyhow::Error bisa menampung tipe error apa pun yang mengimplementasikan trait Error standar. Jadi ? bisa dipakai untuk semuanya, bahkan saat satu fungsi menemui beberapa tipe error yang berbeda:
use anyhow::Result;
// Dua tipe error yang berbeda: ParseIntError dan ParseFloatError.// anyhow::Result menerima keduanya lewat `?`.fn total_price(quantity: &str, price: &str) -> Result<f64> { let quantity: u32 = quantity.trim().parse()?; let price: f64 = price.trim().parse()?; Ok(quantity as f64 * price)}
fn main() { for (quantity, price) in [("3", "2.50"), ("tiga", "2.50"), ("3", "murah")] { match total_price(quantity, price) { Ok(total) => println!("total: {total}"), Err(e) => println!("error: {e}"), } }}Output:
total: 7.5error: invalid digit found in stringerror: invalid float literalTanpa anyhow, kamu butuh enum sendiri dengan dua varian, atau Box<dyn Error>. anyhow itu seperti Box<dyn Error> yang lebih nyaman. Tapi perhatikan masalahnya: “invalid digit found in string” tidak memberi tahu nilai mana yang salah. Untuk itulah ada context.
anyhow: menambahkan context dan mencetak rantai error
Section titled “anyhow: menambahkan context dan mencetak rantai error”Impor trait Context, dan setiap Result mendapat dua method baru:
.context("pesan")membungkus error dengan pesan yang tetap..with_context(|| format!(...))menerima closure. Pesannya hanya dibuat kalau memang terjadi error, jadi pakai ini kalau pesannya butuhformat!.
Setiap lapisan context menambah satu mata rantai pada error chain (rantai error). Cara kamu mencetak error menentukan seberapa banyak rantai yang terlihat:
use anyhow::{Context, Result};
fn parse_port(text: &str) -> Result<u16> { let port: u16 = text .parse() .with_context(|| format!("`{text}` bukan port yang valid"))?; Ok(port)}
fn load_settings(port_text: &str) -> Result<u16> { let port = parse_port(port_text).context("gagal memuat pengaturan")?; Ok(port)}
fn main() { let err = load_settings("99999").unwrap_err();
println!("{err}"); // hanya pesan paling luar println!("{err:#}"); // seluruh rantai dalam satu baris
for (i, cause) in err.chain().enumerate() { println!("{i}: {cause}"); }}Output:
gagal memuat pengaturangagal memuat pengaturan: `99999` bukan port yang valid: number too large to fit in target type0: gagal memuat pengaturan1: `99999` bukan port yang valid2: number too large to fit in target type“number too large to fit in target type” adalah pesan Rust sendiri: angkanya terlalu besar untuk tipe tujuannya (u16 maksimal 65535).
Format ketiga adalah {:?}. Format ini mencetak laporan beberapa baris, dan itulah yang kamu lihat saat main mengembalikan error:
println!("{err:?}");gagal memuat pengaturan
Caused by: 0: `99999` bukan port yang valid 1: number too large to fit in target typeJudul “Caused by:” (disebabkan oleh) ditulis oleh anyhow sendiri, jadi selalu dalam bahasa Inggris.
Kalau environment variable RUST_BACKTRACE=1 di-set, {:?} juga menambahkan stack backtrace yang menunjukkan di mana error itu dibuat. Lihat Environment dan proses.
Contoh pemakaian nyata yang umum adalah membaca file, karena pesan dari sistem operasi (“No such file or directory”) tidak pernah menyebutkan file mana:
let text = std::fs::read_to_string(path) .with_context(|| format!("gagal membaca file konfigurasi {path}"))?;anyhow: bail!, ensure!, dan anyhow!
Section titled “anyhow: bail!, ensure!, dan anyhow!”Kadang tidak ada error yang mendasarinya. Kamu sendiri yang memutuskan bahwa ada yang salah. anyhow punya tiga makro untuk itu:
bail!("...")langsung mengembalikan error saat itu juga. Ini singkatan darireturn Err(anyhow!("...")).ensure!(kondisi, "...")mengembalikan error kalau kondisinya bernilai false.anyhow!("...")membuat nilai error tanpa mengembalikannya.
Ketiganya menerima pesan dengan gaya format!.
use anyhow::{anyhow, bail, ensure, Result};
fn withdraw(balance: u32, amount: u32) -> Result<u32> { ensure!(amount > 0, "jumlah harus lebih dari nol"); if amount > balance { bail!("saldo tidak cukup: saldo {balance}, mencoba mengambil {amount}"); } Ok(balance - amount)}
fn find_user(id: u32) -> Result<&'static str> { match id { 1 => Ok("Ferris"), _ => Err(anyhow!("tidak ada pengguna dengan id {id}")), }}
fn main() { println!("{:?}", withdraw(100, 30).ok()); for (balance, amount) in [(100, 0), (100, 250)] { if let Err(e) = withdraw(balance, amount) { println!("error: {e}"); } } println!("{}", find_user(1).unwrap()); println!("error: {}", find_user(7).unwrap_err());}Output:
Some(70)error: jumlah harus lebih dari nolerror: saldo tidak cukup: saldo 100, mencoba mengambil 250Ferriserror: tidak ada pengguna dengan id 7Mengembalikan anyhow::Result dari main
Section titled “Mengembalikan anyhow::Result dari main”Tempat paling umum kamu melihat anyhow adalah di sini: main mengembalikan anyhow::Result<()>, dan kamu memakai ? di mana-mana. Kalau sebuah error sampai ke main, Rust mencetaknya dengan {:?} ke stderr dan keluar dengan kode 1:
use anyhow::{Context, Result};
fn main() -> Result<()> { let text = "empat puluh dua"; let n: i32 = text .parse() .with_context(|| format!("tidak bisa membaca {text:?} sebagai angka"))?; println!("{n}"); Ok(())}Tidak ada yang dicetak ke stdout. Di stderr kamu akan melihat:
Error: tidak bisa membaca "empat puluh dua" sebagai angka
Caused by: invalid digit found in stringMemakai keduanya bersamaan
Section titled “Memakai keduanya bersamaan”Berikut bentuk umum sebuah proyek sungguhan. Modul inventory berperan sebagai library: ia mengembalikan InventoryError yang presisi, dibuat dengan thiserror. Kode aplikasinya memakai anyhow, menambahkan context, dan hanya melihat isi error di tempat yang benar-benar penting. downcast_ref bertanya pada sebuah anyhow::Error, “apakah sebenarnya kamu tipe ini?”, dan tetap bekerja meskipun sudah ditambahi context:
// ----- bagian "library": error yang presisi dengan thiserror -----mod inventory { use thiserror::Error;
#[derive(Debug, Error)] pub enum InventoryError { #[error("item `{0}` tidak dikenal")] UnknownItem(String), #[error("hanya tersisa {available}, tidak bisa mengambil {wanted}")] NotEnough { available: u32, wanted: u32 }, }
pub struct Store { pub apples: u32, }
impl Store { pub fn take(&mut self, item: &str, wanted: u32) -> Result<(), InventoryError> { if item != "apel" { return Err(InventoryError::UnknownItem(item.to_string())); } if wanted > self.apples { return Err(InventoryError::NotEnough { available: self.apples, wanted }); } self.apples -= wanted; Ok(()) } }}
// ----- bagian "aplikasi": anyhow + context -----use anyhow::{Context, Result};use inventory::{InventoryError, Store};
fn process_order(store: &mut Store, id: u32, item: &str, amount: u32) -> Result<()> { store .take(item, amount) .with_context(|| format!("pesanan #{id} gagal"))?; println!("pesanan #{id} selesai"); Ok(())}
fn main() { let mut store = Store { apples: 5 }; let orders = [(1, "apel", 2), (2, "plum", 1), (3, "apel", 9)];
for (id, item, amount) in orders { if let Err(e) = process_order(&mut store, id, item, amount) { println!("error: {e:#}"); // Kita masih bisa melihat tipe thiserror aslinya di dalam: if let Some(InventoryError::NotEnough { available, .. }) = e.downcast_ref::<InventoryError>() { println!(" petunjuk: coba pesan {available} atau kurang"); } } } println!("sisa apel: {}", store.apples);}Output:
pesanan #1 selesaierror: pesanan #2 gagal: item `plum` tidak dikenalerror: pesanan #3 gagal: hanya tersisa 3, tidak bisa mengambil 9 petunjuk: coba pesan 3 atau kurangsisa apel: 3Kesalahan umum
Section titled “Kesalahan umum”Lupa mengimpor Context. .context() adalah method dari trait anyhow::Context, dan method milik trait hanya bisa dipakai kalau trait-nya ada di dalam scope:
use anyhow::Result;
fn parse(text: &str) -> Result<i32> { let n: i32 = text.parse().context("not a number")?; Ok(n)}error[E0599]: no method named `context` found for enum `Result<T, E>` in the current scope --> src/main.rs:4:31 |4 | let n: i32 = text.parse().context("not a number")?; | ^^^^^^^ | = help: items from traits can only be used if the trait is in scopehelp: trait `Context` which provides `context` is implemented but not in scope; perhaps you want to import it |1 + use anyhow::Context; |Compiler bilang method context tidak ditemukan, karena trait Context belum ada di scope, dan ia bahkan menyarankan baris use yang kurang. Perbaikannya: use anyhow::{Context, Result};.
Mengulang pesan source di dalam pesanmu sendiri. Dengan #[from], error di dalamnya sudah menjadi source. Kalau kamu juga menaruh {0} di pesannya, laporan lengkap akan mencetaknya dua kali:
#[derive(Debug, thiserror::Error)]enum AppError { #[error("port tidak valid: {0}")] // memuat teks dari source... BadPort(#[from] ParseIntError), // ...dan ini juga source-nya}Kalau dicetak dengan {:#} lewat anyhow, hasilnya port tidak valid: invalid digit found in string: invalid digit found in string. Perbaikannya: tulis #[error("port tidak valid")] dan biarkan rantai error yang menampilkan penyebabnya, atau tetap pakai {0} tapi jangan tandai field itu sebagai source. Pilih salah satu.
Mencetak dengan {} lalu kehilangan penyebabnya. println!("{e}") pada anyhow::Error hanya menampilkan pesan paling luar, misalnya “gagal memuat pengaturan”. Saat mencetak error untuk dibaca manusia, pakai {:#} (satu baris) atau {:?} (laporan lengkap).
Memakai anyhow di API publik sebuah library. Kalau library-mu mengembalikan anyhow::Result, pemanggilnya tidak bisa men-match error-mu. Mereka harus menebak tipenya dengan downcast_ref. Beri fungsi publik enum dari thiserror, dan simpan anyhow untuk aplikasi, test, dan contoh.
Ringkasan
Section titled “Ringkasan”- thiserror untuk mendefinisikan error:
#[derive(Error)]ditambah pesan#[error("...")]di setiap varian menggantikanimpl Displaydanimpl Erroryang kamu tulis manual. #[from]membuat?mengubah error di dalamnya menjadi varianmu dan mencatatnya sebagai source.#[source]mencatat source tanpa konversi itu.- anyhow untuk meneruskan error ke atas:
anyhow::Result<T>menerima error apa pun lewat?. .context()dan.with_context()menambahkan lapisan yang mudah dibaca manusia ke rantai error. Cetak rantainya dengan{:#}(satu baris) atau{:?}(laporan).bail!langsung mengembalikan error,ensure!mengembalikan error kalau sebuah kondisi bernilai false, dananyhow!membuat error.- Susunan yang umum: thiserror di kode library, anyhow di
main.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Kamu sedang menulis library, dan pemanggilnya perlu bereaksi berbeda untuk timeout dan input yang salah. Crate mana yang paling cocok?
2. Apa yang dilakukan #[from] pada field sebuah varian?
3. Format mana yang mencetak anyhow::Error beserta seluruh rantainya dalam satu baris?
4. Kapan sebaiknya kamu memakai .with_context(|| ...) alih-alih .context(...)?
5. Apa yang dilakukan ensure!(amount > 0, "jumlah harus positif") saat amount bernilai 0?
Skor: 0 / 5
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- anyhow documentation — docs.rs (bahasa Inggris): API lengkapnya, termasuk
Context,chain(), dan downcasting. - thiserror documentation — docs.rs (bahasa Inggris): semua atribut, seperti
#[error(transparent)]dan field backtrace. - The Context trait — anyhow (bahasa Inggris): cara kerja
contextdanwith_context, lengkap dengan contoh. - Error Handling — The Rust Book (bahasa Inggris): dasar-dasar penanganan error yang menjadi fondasi kedua crate ini.
- std::error::Error (bahasa Inggris): trait standar di balik
.source()dan rantai error.