Section Penanganan Error mengajari kita membangun enum error sendiri lengkap dengan implementasi Display dan konversi From — pola yang benar, tetapi menulis semuanya manual terasa berulang kali lebih panjang daripada logika bisnisnya. Kebutuhan error juga berbeda menurut posisi kode: aplikasi menginginkan pesan yang kaya konteks dan mudah mengalir ke atas, sedangkan library wajib mengekspos tipe error yang bisa di-match oleh pemanggilnya. Dua crate paling populer di ekosistem membagi tugas tersebut dengan rapi: anyhow menyederhanakan error di sisi aplikasi, thiserror menyederhanakan pembuatan tipe error di sisi library. Pada Chapter ini kita memakai keduanya dalam proyek sungguhan dan melihat bagaimana keduanya saling melengkapi, bukan bersaing.
Dua Peran yang Berbeda #
Sebelum menulis kode, mari kita tegaskan pembagian perannya, karena memilih crate yang salah untuk peran yang salah adalah kesalahan paling umum:
| Aspek | anyhow | thiserror |
|---|---|---|
| Sasaran | Aplikasi (binari akhir) | Library (crate yang dikonsumsi orang lain) |
| Bentuk error | Satu tipe seragam menampung error apa pun | Enum terketik dengan variant eksplisit |
| Konteks | context() menambah keterangan berlapis-lapis | Keterangan ditanam di tiap variant |
| Kapan dipilih | Ketika pemanggil hanya perlu melaporkan error | Ketika pemanggil perlu bereaksi pada jenis error |
Alasan pembagian tersebut murni soal kontrak API. Library yang mengembalikan anyhow::Error memaksa seluruh pemanggilnya ke satu tipe opaq — pemanggil tidak bisa lagi membedakan “berkas tidak ada” dari “format salah” tanpa membandingkan teks pesan. Sebaliknya, aplikasi jarang perlu memilah jenis error secara programatik; aplikasi cukup melaporkan error sebaik mungkin kepada pengguna, dan di situlah konteks berlapis milik anyhow bersinar.
anyhow untuk Aplikasi #
Kita mulai dari masalah yang nyata. Fungsi std::fs::read_to_string mengembalikan std::io::Error yang jujur tetapi miskin: pesannya hanya “No such file or directory”, tanpa menyebut berkas mana yang gagal dibaca. Aplikasi yang memakai Box<dyn Error> dan operator ? bisa mengalirkan error ke atas, tetapi setiap lapisan fungsi membuang konteks yang justru paling berharga saat debug.
anyhow menjawabnya dengan tiga pilar. Pertama, tipe anyhow::Error mampu menampung error apa pun yang mengimplementasikan std::error::Error — std error, error dari crate lain, hingga enum buatan kita sendiri. Kedua, trait Context memberi metode context() dan with_context() yang membungkus error dengan keterangan baru tanpa menghapus error aslinya, membentuk error chain. Ketiga, alias anyhow::Result<T> menyederhanakan signature sehingga Result<T, anyhow::Error> cukup ditulis pendek.
Mari kita bangun aplikasi pembaca berkas konfigurasi. Buat proyek baru lalu tambahkan dependency:
cargo new baca_setelan
cd baca_setelan
cargo add anyhowManifest proyek kini memuat satu dependency:
[package]
name = "baca_setelan"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.104"Kode lengkap src/main.rs berisi dua fungsi berlapis yang sama-sama menambahkan konteks:
use anyhow::{Context, Result};
fn baca_berkas(nama: &str) -> Result<String> {
std::fs::read_to_string(nama).context(format!("gagal membaca berkas '{nama}'"))
}
fn ambil_setelan() -> Result<String> {
let isi = baca_berkas("setelan.txt").context("memuat setelan aplikasi gagal")?;
Ok(isi.trim().to_string())
}
fn main() -> Result<()> {
let setelan = ambil_setelan()?;
println!("Setelan aktif: {setelan}");
Ok(())
}Berkas setelan.txt belum kita buat, jadi eksekusi pertama justru memperlihatkan kekuatan utama anyhow. Jalankan program dalam keadaan berkas tidak ada:
cargo runOutput yang Dihasilkan:
Error: memuat setelan aplikasi gagal
Caused by:
0: gagal membaca berkas 'setelan.txt'
1: No such file or directory (os error 2)Teks “No such file or directory” dari std::io::Error masih utuh di baris terakhir — anyhow tidak membuang error apa pun. Sekarang ciptakan berkasnya dan jalankan ulang:
echo "mode produksi" > setelan.txt
cargo runOutput yang Dihasilkan:
Setelan aktif: mode produksiMari kita bedah kode di atas:
- Baris 1 (
use anyhow::{Context, Result};): mengimpor aliasResult— versistd::result::Resultdengan tipe error tetapanyhow::Error— serta traitContextpemilik metodecontext(). Tanpa trait tersebut di scope, pemanggilancontext()tidak akan diketemukan. - Baris 3 (
fn baca_berkas(nama: &str) -> Result<String>): signature pendek hasil alias anyhow. Fungsi ini mengembalikan isi berkas, atau error bertipeanyhow::Error. - Baris 4 (
std::fs::read_to_string(nama).context(...)): hasilread_to_stringbertipeResult<String, std::io::Error>;context()mengubah tipe error menjadianyhow::Errorsembari membungkusstd::io::Errordi dalamnya. Argumenformat!dipakai karenanamaharus ikut tercetak di pesan. - Baris 8 (
let isi = baca_berkas("setelan.txt").context("memuat setelan aplikasi gagal")?;): lapisan kedua menambahkan keterangan lain di atas hasil lapisan pertama. Operator?tetap bekerja seperti biasa — hanya tipe error-nya sudahanyhow::Error, jadi tidak ada konversi tambahan. - Baris 12 (
fn main() -> Result<()>):mainboleh mengembalikanResultsejak Rust 1.26. Ketika program berakhir denganErr, runtime Rust mencetak isi error tersebut dengan formatDebug— dan implementasiDebugmilikanyhow::Errorinilah yang menghasilkan blokCaused byberisi seluruh error chain. - Baris 13 (
let setelan = ambil_setelan()?;): propagasi terakhir. Karenamainsendiri bertipeResult<()>, operator?menyerahkan error langsung ke runtime, bukan kepanic!.
Bagian Caused by tersusun dari lapisan terluar ke error paling dasar: keterangan ambil_setelan, keterangan baca_berkas, lalu pesan asli std::io::Error. Tiga lapisan itu terbentuk dari dua pemanggilan context() — rantai lengkapnya tampil tanpa satu baris kode pencetakan manual.
thiserror untuk Library #
Sekarang kita pindah kursi menjadi penulis library. Konsumen library kantin kita ingin membedakan “menu tidak ada” dari “input jumlah salah” — dua keadaan yang mungkin perlu reaksi berbeda di kode mereka. Enum error adalah jawabannya, dan Section Penanganan Error sudah menunjukkan harganya: implementasi Display manual, implementasi std::error::Error manual, dan From untuk tiap error bawaan yang dibungkus — semua itu berulang secara mekanis.
thiserror memangkas boilerplate tersebut lewat derive macro. Atribut #[derive(Error)] mengimplementasikan std::error::Error secara otomatis; atribut #[error("...")] pada tiap variant menghasilkan implementasi Display dengan sintaks format yang sudah kita kenal dari println!; dan atribut #[from] menghasilkan implementasi From yang sekaligus menjadikan field tersebut sebagai source — error penyebab di rantai. Buat proyeknya:
cargo new pesanan_kantin
cd pesanan_kantin
cargo add thiserror[package]
name = "pesanan_kantin"
version = "0.1.0"
edition = "2024"
[dependencies]
thiserror = "2.0.21"Kode src/main.rs — anggap bagian enum nantinya hidup di crate library, bukan di berkas main:
use std::error::Error as StdError;
use std::num::ParseIntError;
use thiserror::Error;
#[derive(Debug, Error)]
enum KesalahanPesanan {
#[error("pesanan #{id} tidak ditemukan di daftar menu")]
TidakDitemukan { id: u32 },
#[error("jumlah pesanan tidak valid: {pesan}")]
JumlahTidakValid { pesan: String },
#[error("gagal membaca jumlah sebagai angka")]
AngkaTidakValid(#[from] ParseIntError),
}
fn cari_menu(id: u32) -> Result<&'static str, KesalahanPesanan> {
match id {
1 => Ok("Nasi goreng spesial"),
2 => Ok("Es teh manis"),
_ => Err(KesalahanPesanan::TidakDitemukan { id }),
}
}
fn baca_jumlah(teks: &str) -> Result<u32, KesalahanPesanan> {
let jumlah: u32 = teks.trim().parse()?;
if jumlah == 0 {
return Err(KesalahanPesanan::JumlahTidakValid {
pesan: "jumlah minimal satu porsi".to_string(),
});
}
Ok(jumlah)
}
fn main() {
println!("{}", cari_menu(1).unwrap());
println!("{}", cari_menu(9).unwrap_err());
println!("{}", baca_jumlah("tiga").unwrap_err());
if let Err(e) = baca_jumlah("tiga") {
if let Some(sumber) = e.source() {
println!("Penyebab: {sumber}");
}
}
}Output yang Dihasilkan:
Nasi goreng spesial
pesanan #9 tidak ditemukan di daftar menu
gagal membaca jumlah sebagai angka
Penyebab: invalid digit found in stringMari kita bedah bagian-bagian kuncinya:
- Baris 5 (
#[derive(Debug, Error)]): dua derive sekaligus —Debugstandar untuk kebutuhan pencetakan, danErrormilik thiserror yang mengimplementasikanstd::error::Erroruntuk enum kita. - Baris 7 (
#[error("pesanan #{id} tidak ditemukan di daftar menu")]): teks di dalam atribut menjadi isi implementasiDisplayuntuk variantTidakDitemukan. Penanda{id}mengambil fieldidmilik variant tersebut, persis seperti formatprintln!. - Baris 13-14 (
#[error("gagal membaca jumlah sebagai angka")]danAngkaTidakValid(#[from] ParseIntError)): variant tuple yang membungkusParseIntError. Atribut#[from]menghasilkan dua hal sekaligus — implementasiFrom<ParseIntError>sehingga operator?pada baris 26 bekerja otomatis, dan penandaan field sebagai error penyebab sehingga metodesource()mengembalikannya. - Baris 40-41 (
if let Err(e) = ... e.source()): pemanggil library memeriksa rantai penyebab secara manual.source()berasal dari traitstd::error::Erroryang diimpor sebagaiStdErrordi baris 1, dan mengembalikanOptionkarena tidak semua variant punya penyebab —TidakDitemukandanJumlahTidakValidadalah error mandiri. - Baris 26 (
let jumlah: u32 = teks.trim().parse()?;):parsemengembalikanParseIntErrorsaat gagal;#[from]mengubahnya menjadiKesalahanPesanan::AngkaTidakValidsebelum error keluar dari fungsi. Pemanggil hanya melihat tipe error library kita, bukan tipe error bawaan yang kebetulan kita pakai di dalam.
Perhatikan baris output kedua dan ketiga: unwrap_err() mengembalikan nilai error, lalu {} mencetaknya dengan format Display — teks yang persis berasal dari atribut #[error("...")]. Semua trik pencetakan pesan di Section Penanganan Error kini dihasilkan derive, tanpa satu baris impl Display pun.
Keduanya Bekerja Bersama #
Pola dunia nyata menggabungkan keduanya: library mengekspos enum thiserror, aplikasi menelan error tersebut ke dalam anyhow. Konversinya otomatis — anyhow::Error mengimplementasikan From untuk semua tipe yang mengimplementasikan std::error::Error, dan enum thiserror sudah memenuhi syarat tersebut. Proyek berikut memasang kedua crate sekaligus:
cargo new aplikasi_kantin
cd aplikasi_kantin
cargo add anyhow thiserror[package]
name = "aplikasi_kantin"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.104"
thiserror = "2.0.21"use anyhow::{Context, Result};
use std::num::ParseIntError;
use thiserror::Error;
#[derive(Debug, Error)]
enum KesalahanKantin {
#[error("jumlah porsi tidak boleh nol")]
JumlahKosong,
#[error("gagal membaca jumlah sebagai angka")]
AngkaTidakValid(#[from] ParseIntError),
}
fn baca_jumlah(teks: &str) -> std::result::Result<u32, KesalahanKantin> {
let jumlah: u32 = teks.trim().parse()?;
if jumlah == 0 {
return Err(KesalahanKantin::JumlahKosong);
}
Ok(jumlah)
}
fn muat_pesanan(id: u32, teks_jumlah: &str) -> Result<u32> {
let jumlah = baca_jumlah(teks_jumlah)
.with_context(|| format!("gagal memuat pesanan #{id}"))?;
Ok(jumlah)
}
fn main() -> Result<()> {
let jumlah = muat_pesanan(7, "tiga")?;
println!("Jumlah porsi: {jumlah}");
Ok(())
}Output yang Dihasilkan:
Error: gagal memuat pesanan #7
Caused by:
0: gagal membaca jumlah sebagai angka
1: invalid digit found in stringTiga baris rantai tersebut lahir dari tiga sumber berbeda: teks “gagal memuat pesanan #7” ditambahkan with_context di sisi aplikasi, teks “gagal membaca jumlah sebagai angka” berasal dari atribut #[error] di sisi library, dan baris terakhir adalah ParseIntError dari standard library. Perhatikan pula signature-nya: baca_jumlah tetap mengembalikan tipe error library yang terketik, sementara muat_pesanan bebas mengubahnya menjadi anyhow::Result begitu konteks aplikasi menempel. Pemanggilan with_context(|| ...) memakai closure agar format! hanya dieksekusi ketika memang terjadi error.
Ringkasan #
anyhow dan thiserror membagi wilayah kerja penanganan error: anyhow::Error menampung error apa pun di sisi aplikasi, context() menumpuk keterangan lapis demi lapis, dan implementasi Debug-nya mencetak seluruh error chain lengkap dengan blok Caused by. Di sisi library, #[derive(Error)] thiserror menggantikan impl Display dan impl Error manual, #[error("...")] mendefinisikan pesan tiap variant, dan #[from] menghubungkan error bawaan sebagai penyebab. Keduanya bertemu secara alami karena anyhow menerima semua tipe error standar — library tetap terketik, aplikasi tetap luwes. Chapter berikutnya melanjutkan ke crate yang mengurus pintu masuk setiap aplikasi Rust: parser argumen baris perintah bernama clap.