Box<dyn Error> nyaman karena menerima semua error, tetapi kenyamanan tersebut ada harganya: seluruh kegagalan melebur menjadi satu wadah generik, dan pemanggil tidak lagi bisa membedakan “kolom usia kosong” dari “usia di luar rentang” tanpa membaca dan memilah teks pesannya. Aplikasi nyata sering butuh lebih dari itu — layar formulir ingin menyorot kolom yang salah, dan log ingin mencatat kode kegagalan yang bisa dihitung. Solusinya tidak memerlukan mekanisme bahasa baru: cukup enum yang kita definisikan sendiri, lengkap dengan variant yang membawa data konteks, dan satu implementasi Display agar error kita bisa dicetak dengan rapi. Semua bekal kita dari section Struktur Data — enum dengan data, pattern matching, method lewat impl — bekerja penuh di sini. Pada chapter ini kita akan membangun tipe AgeError untuk validasi usia, dari definisi enum sampai pemanggil yang mengambil keputusan per variant.
Enum Error dengan Data Konteks #
Validasi usia memiliki tiga cara gagal yang maknanya berbeda: kolom dibiarkan kosong, teks bukan angka, atau angkanya berada di luar rentang wajar manusia. Ketiganya kita angkat menjadi variant enum:
enum AgeError {
Empty,
NotNumber,
OutOfRange(u32),
}Variant OutOfRange(u32) adalah bintang utamanya: variant tersebut membawa data konteks — angka usia yang melanggar batas — sehingga pemanggil tidak hanya tahu bahwa rentang dilanggar, tetapi juga tahu angka berapa yang melanggar. Enum dengan data seperti ini adalah alat yang tepat untuk tipe error karena satu enum sudah mewakili “semua cara domain ini bisa gagal”, dan kompilator akan menolak match yang lupa menangani salah satu cara. Bandingkan dengan String sebagai tipe error: pesan bebas bisa disimpan, tetapi tidak ada yang menjamin pemanggil memeriksa semua jenis kegagalan.
Fungsi validasinya memetakan tiap kondisi gagal ke variant yang sesuai:
fn parse_age(input: &str) -> Result<u32, AgeError> {
if input.is_empty() {
return Err(AgeError::Empty);
}
let usia: u32 = match input.parse() {
Ok(nilai) => nilai,
Err(_) => return Err(AgeError::NotNumber),
};
if usia > 120 {
return Err(AgeError::OutOfRange(usia));
}
Ok(usia)
}Mari kita bedah kode di atas:
- Baris 1 (
fn parse_age(input: &str) -> Result<u32, AgeError>): tipe kegagalan pada tanda tangan kini milik kita sendiri. Pemanggil yang membaca tanda tangan langsung tahu bahwa kegagalan berarti salah satu dari tiga variantAgeError— bukan kegagalan parsing mentah. - Baris 2-4 (
if input.is_empty() ... AgeError::Empty): kondisi pertama diperiksa paling depan; teks kosong pulang membawa variant tanpa data. - Baris 6-9 (
match input.parse() { ... Err(_) => ... }): kegagalan parsingParseIntErrorditerjemahkan menjadi variantAgeError::NotNumber. PolaErr(_)mengabaikan isi error aslinya karena pemanggil cukup tahu “teks ini bukan angka”. - Baris 11-13 (
if usia > 120 ... AgeError::OutOfRange(usia)): kegagalan domain yang tidak dimilikiparsesama sekali — angka valid secara sintaks tetapi tidak masuk akal secara domain. Variant membawa angkausiasebagai konteks. - Baris 15 (
Ok(usia)): jalur sukses menutup kemungkinan dengan nilai yang sudah tervalidasi penuh.
Perhatikan pemisahan tanggung jawab yang terbentuk: parse menangani sintaks, fungsi kita menangani domain, dan tipe AgeError menyimpan kosakata kegagalan domain tersebut.
Mengimplementasikan Display untuk Pesan yang Rapi #
Tipe error yang baik harus bisa mencetak dirinya sendiri. Kemampuan tersebut diberikan oleh trait Display — trait yang sama yang membuat println!("{}", nilai) bekerja pada String maupun ParseIntError. Kita implementasikan trait tersebut untuk AgeError dengan macro write!:
use std::fmt;
impl fmt::Display for AgeError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AgeError::Empty => write!(f, "kolom usia masih kosong"),
AgeError::NotNumber => write!(f, "usia harus berupa angka"),
AgeError::OutOfRange(usia) => write!(f, "usia {} di luar rentang 0-120", usia),
}
}
}Uraian komponennya:
- Baris 3 (
impl fmt::Display for AgeError): blokimplmenyatakan bahwa mulai sekarangAgeErrormengimplementasikan traitDisplaymilik modulstd::fmt. Struktur blok ini sama denganimplmethod yang kita kenal, hanya dengan klausaforyang menyebut trait-nya. - Baris 4 (
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result): method wajib traitDisplay. Parameterfadalah formatter — tujuan penulisan teks — dan nilai kembaliannyafmt::Resultyang menyatakan apakah penulisan berhasil. Nama method dan bentuk parameternya tidak boleh kita ubah; trait yang menentukan. - Baris 5-9 (
match self { ... }): pencocokan pola atas variant — kompiler kembali memastikan ketiga jenis kegagalan tertangani. VariantOutOfRange(usia)mengikat angka konteks ke variabelusiauntuk disisipkan ke pesan. write!(f, ...): sepupuprintln!yang menulis ke formatterfalih-alih ke layar, lengkap dengan dukungan placeholder{}. Nilai kembalianfmt::Resultmenjadi nilai kembalian method secara implisit — tidak perlu titik koma di barismatch.
Satu paket lengkapnya kini bisa kita uji:
fn main() {
let inputs = ["34", "", "abc", "150"];
for input in inputs {
match parse_age(input) {
Ok(usia) => println!("Input '{}' valid, usia {}", input, usia),
Err(error) => println!("Input '{}' ditolak: {}", input, error),
}
}
}Output yang Dihasilkan:
Input '34' valid, usia 34
Input '' ditolak: kolom usia masih kosong
Input 'abc' ditolak: usia harus berupa angka
Input '150' ditolak: usia 150 di luar rentang 0-120Baris Err(error) => println!("... {}", error) bekerja karena implementasi Display kita: tiap variant mencetak pesan yang sesuai, termasuk angka 150 yang tersimpan di dalam OutOfRange.
Pemanggil yang Mengambil Keputusan per Variant #
Kekuatan sebenarnya dari enum error muncul ketika pemanggil memerlukan lebih dari pesan: pemanggil bisa mencocokkan variant-nya langsung dan membaca data konteksnya. Layar formulir, misalnya, ingin menampilkan penolakan khusus untuk usia yang melampaui batas:
fn main() {
match parse_age("150") {
Ok(usia) => println!("Pendaftaran usia {} diterima", usia),
Err(AgeError::OutOfRange(batas)) => println!("Usia {} tahun ditolak, batas maksimal 120", batas),
Err(error) => println!("Ditolak: {}", error),
}
}Output yang Dihasilkan:
Usia 150 tahun ditolak, batas maksimal 120Mari kita amati pola pada pencocokannya:
- Baris 4 (
Err(AgeError::OutOfRange(batas))): pola bertingkat —Errdibuka lebih dulu, lalu variantOutOfRangedibuka lagi untuk mengikat isi konteksnya kebatas. Angka150kini menjadi nilai biasa yang dipakai program, bukan sekadar potongan teks pesan. - Baris 5 (
Err(error)): pola penampung untuk dua variant sisanya; variabelerrormemegang seluruh nilaiAgeErrordan mencetaknya lewatDisplay. Tanpa pola ini, kompilator menolakmatchkarenaEmptydanNotNumberbelum tertangani.
Kemampuan inilah yang membedakan tipe error kustom dari String atau Box<dyn Error>: keputusan berdasarkan jenis kegagalan dievaluasi oleh kompilator, bukan oleh pembandingan teks pesan. Menambah cara gagal baru — misalnya UnderAge(u32) untuk usia di bawah batas minimal — otomatis memaksa seluruh match di seluruh kode diperbarui; tidak ada satu pun cabang yang bisa terlupa diam-diam.
Ringkasan #
Tipe error aplikasi didefinisikan sebagai enum dengan variant per jenis kegagalan, dan variant dapat membawa data konteks seperti OutOfRange(u32) yang menyimpan angka pelanggar. Fungsi parse_age menerjemahkan kegagalan sintaks dari parse menjadi variant domain, implementasi Display dengan write! memberi tiap variant pesan yang rapi, dan pemanggil dapat mencocokkan pola bertingkat Err(AgeError::OutOfRange(batas)) untuk mengambil keputusan berbasis jenis kegagalan beserta datanya. Kompilator menjaga ketuntasan penanganan sejak definisi enum sampai cabang terakhir. Seluruh perkakas section ini — panic, Result, keluarga unwrap_or, map/map_err, operator ?, Box<dyn Error>, dan enum error kustom — kini lengkap; satu pertanyaan penutup tersisa: untuk kegagalan baru berikutnya, alat mana yang sebaiknya kita pilih? Chapter terakhir menyusun kerangka keputusannya.