Topik ini membahas arsitektur error pada tingkat aplikasi melalui tiga komponen pembentuknya, yaitu trait std::error::Error sebagai kontrak dasar tipe error, metode source yang merangkai penyebab kegagalan menjadi rantai penyebab, dan fungsi pelaporan yang menyusuri rantai itu untuk mencetak laporan berlapis di titik paling luar program. Seluruh contoh ditulis hanya dengan pustaka standar sehingga dapat dikompilasi langsung oleh rustc, dan pembahasan thiserror serta anyhow dipersingkat karena kedua crate itu sudah dibahas pada modul penanganan error. Setelah membaca topik ini, tipe error berlapis dapat dirancang dengan rantai source yang benar, dan laporan berbentuk daftar penyebab dapat dihasilkan tanpa dependensi eksternal.
Trait Error sebagai fondasi#
Trait std::error::Error adalah penanda bahwa sebuah tipe layak menjadi nilai kegagalan pada jalur Result. Trait ini mewarisi Display dan Debug, sehingga setiap tipe error selalu menyediakan dua bentuk tampilan. Display menghasilkan pesan ringkas yang dibaca manusia, sedangkan Debug menghasilkan bentuk struktur lengkap untuk log dan pengujian. Metode source secara bawaan mengembalikan None, metode description yang lebih tua sudah tidak dianjurkan karena perannya berpindah ke Display, dan metode provide yang memungkinkan data terstruktur dilampirkan tanpa mengubah tipe error masih memerlukan toolchain nightly sehingga belum dapat dipakai pada kode stabil.
Karena Error merupakan trait, nilai error dari lapisan apa pun dapat diperlakukan sebagai satu tipe yang sama, yaitu trait object dyn Error yang lazim disimpan di dalam Box<dyn Error>. Bentuk itu membuat source menjadi antarmuka bersama yang dapat dipanggil tanpa mengetahui tipe konkret yang sedang dipegang. Dalam praktik, bagian yang perlu ditulis tangan hanya ada dua. Pertama, implementasi Display yang menentukan kalimat pesan kegagalan pada lapisan tersebut. Kedua, implementasi source yang menautkan tipe error dari lapisan di bawahnya sebagai penyebab. Pekerjaan mekanis inilah yang dituliskan otomatis oleh thiserror pada kode pustaka, sehingga pemahaman bentuk manualnya menjadi prasyarat sebelum memakai alat bantu semacam itu.
Rantai penyebab dan batas ‘static#
Metode source mengembalikan Option<&(dyn Error + 'static)>. Nilai None menyatakan bahwa error tersebut adalah ujung rantai, sedangkan Some memuat penyebab dari lapisan di bawahnya. Tipe kembalian berupa referensi pinjaman karena penyebab tidak dilepas dari pemiliknya, melainkan disimpan sebagai field pada enum error lapisan atas lalu dipinjamkan melalui source. Batas 'static menegaskan bahwa penyebab bukan referensi ke data sementara, dan jaminan inilah yang membuat Box<dyn Error> dapat menyimpan rantai utuh serta mendukung downcast, yakni usaha memulihkan tipe konkret dari nilai bertipe trait object.
Rantai penyebab yang terbentuk menyerupai daftar berantai yang arahnya menuju lapisan terbawah. Setiap lapisan hanya menuliskan apa yang sedang dikerjakannya, bukan mengulang pesan lapisan di bawahnya, karena laporan pada titik luar akan mencetak keduanya secara berurutan. Pada pemuatan pengaturan misalnya, lapisan terbawah ParseIntError menyimpan detail teknis dari kegagalan parse, lapisan domain menambahkan kunci serta teks yang gagal, dan lapisan aplikasi menyatakan bahwa pemuatan pengaturan secara keseluruhan tidak berhasil.
Laporan berlapis dari titik paling luar#
Titik paling luar program adalah tempat satu-satunya keputusan tentang bentuk laporan diambil, karena hanya titik itu yang mengetahui keseluruhan konteks eksekusi. Program berikut membangun pemuat pengaturan dengan dua lapisan error. ErrorPengaturan berada di lapisan domain dan membungkus ParseIntError dari operasi parse. ErrorPemuatan berada di lapisan aplikasi dan membungkus ErrorPengaturan melalui implementasi From, sehingga operator ? dapat mengangkat kegagalan domain menjadi kegagalan aplikasi secara otomatis. Fungsi laporkan kemudian menyusuri rantai source sampai habis dan mencetak setiap penyebab dengan penomoran.
use std::error::Error;
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
struct Pengaturan {
port: u32,
batas: u32,
}
#[derive(Debug)]
enum ErrorPengaturan {
NilaiBukanAngka {
kunci: String,
teks: String,
sumber: ParseIntError,
},
}
impl fmt::Display for ErrorPengaturan {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErrorPengaturan::NilaiBukanAngka { kunci, teks, .. } => {
write!(f, "nilai {} bukan angka: \"{}\"", kunci, teks)
}
}
}
}
impl Error for ErrorPengaturan {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ErrorPengaturan::NilaiBukanAngka { sumber, .. } => Some(sumber),
}
}
}
#[derive(Debug)]
enum ErrorPemuatan {
BarisTanpaPemisah(String),
PengaturanTidakValid { sumber: ErrorPengaturan },
}
impl fmt::Display for ErrorPemuatan {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErrorPemuatan::BarisTanpaPemisah(baris) => {
write!(f, "baris tanpa pemisah \"=\": \"{}\"", baris)
}
ErrorPemuatan::PengaturanTidakValid { .. } => {
write!(f, "pengaturan tidak dapat dimuat")
}
}
}
}
impl Error for ErrorPemuatan {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ErrorPemuatan::BarisTanpaPemisah(_) => None,
ErrorPemuatan::PengaturanTidakValid { sumber } => Some(sumber),
}
}
}
impl From<ErrorPengaturan> for ErrorPemuatan {
fn from(sumber: ErrorPengaturan) -> ErrorPemuatan {
ErrorPemuatan::PengaturanTidakValid { sumber }
}
}
fn angka_dari(kunci: &str, teks: &str) -> Result<u32, ErrorPengaturan> {
teks.parse().map_err(|sumber| ErrorPengaturan::NilaiBukanAngka {
kunci: kunci.to_string(),
teks: teks.to_string(),
sumber,
})
}
fn muat(isi: &str) -> Result<Pengaturan, ErrorPemuatan> {
let mut port = None;
let mut batas = None;
for baris in isi.lines() {
let (kunci, nilai) = baris
.split_once('=')
.ok_or_else(|| ErrorPemuatan::BarisTanpaPemisah(baris.to_string()))?;
match kunci.trim() {
"port" => port = Some(angka_dari("port", nilai.trim())?),
"batas" => batas = Some(angka_dari("batas", nilai.trim())?),
_ => {}
}
}
Ok(Pengaturan {
port: port.unwrap_or(8080),
batas: batas.unwrap_or(100),
})
}
fn laporkan(error: &(dyn Error + 'static)) {
eprintln!("error: {}", error);
let mut nomor = 1;
let mut sumber = error.source();
while let Some(sebab) = sumber {
eprintln!("penyebab {}: {}", nomor, sebab);
nomor += 1;
sumber = sebab.source();
}
}
fn main() {
let isi = "port=delapan\nbatas=100\n";
match muat(isi) {
Ok(pengaturan) => println!("port {}, batas {}", pengaturan.port, pengaturan.batas),
Err(error) => {
laporkan(&error);
std::process::exit(1);
}
}
}error: pengaturan tidak dapat dimuat
penyebab 1: nilai port bukan angka: "delapan"
penyebab 2: invalid digit found in stringTiga baris keluaran itu berasal dari tiga lapisan yang berbeda tanpa satu pun pesan yang ditulis dua kali. Baris error mencetak Display dari ErrorPemuatan, baris penyebab 1 mencetak Display dari ErrorPengaturan beserta konteks kunci dan teksnya, dan baris penyebab 2 mencetak Display dari ParseIntError milik pustaka standar. Perulangan while let pada laporkan bergerak dari satu penyebab ke penyebab berikutnya melalui source, lalu berhenti ketika None ditemukan pada ParseIntError. Pemanggilan std::process::exit(1) memastikan kegagalan terbaca oleh pemanggil eksternal seperti skrip shell. Diagram berikut merangkum struktur rantai yang dilalui fungsi laporan tersebut.
Pemulihan tipe lewat downcast#
Sebagian titik pada batas aplikasi memerlukan keputusan pemulihan, misalnya memakai nilai bawaan ketika kegagalan berasal dari parse, tetapi menolak masukan ketika sumber kegagalannya berbeda. Box<dyn Error> menghapus tipe konkret error yang disimpannya, dan metode downcast_ref::<T>() pada trait object tersebut berusaha memulihkan tipe itu kembali. Usaha ini hanya mungkin karena batas 'static pada source menjamin nilai yang dibungkus dimiliki secara utuh, bukan pinjaman sementara.
use std::error::Error;
use std::num::ParseIntError;
fn baca_angka(masukan: &str) -> Result<u32, Box<dyn Error>> {
let teks = masukan.trim();
if teks.is_empty() {
return Err("masukan kosong".into());
}
Ok(teks.parse()?)
}
fn main() {
for masukan in ["24", "dua puluh empat", " "] {
match baca_angka(masukan) {
Ok(nilai) => println!("{:?} -> {}", masukan, nilai),
Err(error) => {
if let Some(parse) = error.downcast_ref::<ParseIntError>() {
println!("{:?} -> dipakai nilai 0 ({})", masukan, parse);
} else {
println!("{:?} -> ditolak: {}", masukan, error);
}
}
}
}
}"24" -> 24
"dua puluh empat" -> dipakai nilai 0 (invalid digit found in string)
" " -> ditolak: masukan kosongMasukan pertama berhasil melewati parse sehingga jalur Ok tercetak. Masukan kedua gagal sebagai ParseIntError, dan pemanggilan downcast_ref::<ParseIntError>() mengembalikan Some sehingga nilai bawaan nol dipakai. Masukan ketiga menghasilkan error bertipe lain yang dibuat dari &str lewat konversi into, sehingga downcast mengembalikan None dan masukan ditolak. Pemulihan berbasis pencocokan tipe semacam ini tepat dipakai pada batas aplikasi. Untuk API publik pustaka, tipe error konkret yang dapat dicocokkan dengan pola tetap lebih tepat sebagaimana kebiasaan yang dibahas pada modul penanganan error.
Kesalahan umum#
Kesalahan pertama adalah menyatukan seluruh lapisan ke dalam satu enum error raksasa yang memuat variant dari semua lapisan sekaligus. Pendekatan itu menduplikasi variant setiap kali lapisan bawah bertambah, dan perubahan pada satu lapisan merambat ke seluruh pemakai enum tersebut. Setiap lapisan sebaiknya memiliki enum kecil miliknya sendiri, lalu lapisan di atasnya membungkus melalui field sumber beserta implementasi From.
Kesalahan kedua adalah mencetak error sebelum nilai tersebut mencapai titik paling luar. Pencetakan yang tersebar menghasilkan laporan dengan format campuran, dan rantai yang belum terbentuk sepenuhnya tercetak dalam keadaan terpotong. Kembalikan error hingga ke main atau titik laporan lain, lalu biarkan satu fungsi laporan yang menentukan format keluarannya.
Kesalahan ketiga adalah menganggap operator ? dapat mengonversi antar tipe error apa pun secara otomatis. Konversi itu memerlukan implementasi From dari tipe sumber ke tipe target, dan tanpanya kompilator menolak dengan error tipe yang tidak cocok. Tuliskan implementasi From antar lapisan, atau gunakan map_err ketika pembungkusan memerlukan data tambahan yang tidak dimiliki tipe sumber.
Ringkasan#
- Trait
ErrormewarisiDisplaydanDebug.Displaymenyediakan pesan untuk laporan,Debugmenyediakan bentuk lengkap untuk log, dansourceadalah bagian yang ditulis tangan untuk menautkan penyebab. - Rantai penyebab berbentuk daftar berantai menuju lapisan terbawah, dengan batas
'staticyang menjamin penyebab dimiliki penuh sehingga dapat disimpan dalamBox<dyn Error>dan di-downcast. - Laporan berlapis dihasilkan oleh fungsi yang menyusuri
sourcesampaiNone, dipanggil sekali di titik paling luar, dan diikuti kode keluar bukan nol untuk kegagalan. downcast_ref::<T>()memulihkan tipe konkret pada batas aplikasi untuk keputusan pemulihan, sedangkan API publik pustaka tetap memakai tipe error konkret.
Referensi#
- std::error — Rust standard library — Modul error pustaka standar, termasuk panduan error untuk pustaka dan aplikasi.
- Error — Rust standard library
— Dokumentasi trait Error mengenai
source,provide, dandowncast. - Error Handling — The Rust Programming Language
— Bab buku yang menjelaskan filosofi
panicdanResultdi balik arsitektur ini.