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:

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

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

  1. 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 variant AgeError — bukan kegagalan parsing mentah.
  2. Baris 2-4 (if input.is_empty() ... AgeError::Empty): kondisi pertama diperiksa paling depan; teks kosong pulang membawa variant tanpa data.
  3. Baris 6-9 (match input.parse() { ... Err(_) => ... }): kegagalan parsing ParseIntError diterjemahkan menjadi variant AgeError::NotNumber. Pola Err(_) mengabaikan isi error aslinya karena pemanggil cukup tahu “teks ini bukan angka”.
  4. Baris 11-13 (if usia > 120 ... AgeError::OutOfRange(usia)): kegagalan domain yang tidak dimiliki parse sama sekali — angka valid secara sintaks tetapi tidak masuk akal secara domain. Variant membawa angka usia sebagai konteks.
  5. 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!:

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

  1. Baris 3 (impl fmt::Display for AgeError): blok impl menyatakan bahwa mulai sekarang AgeError mengimplementasikan trait Display milik modul std::fmt. Struktur blok ini sama dengan impl method yang kita kenal, hanya dengan klausa for yang menyebut trait-nya.
  2. Baris 4 (fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result): method wajib trait Display. Parameter f adalah formatter — tujuan penulisan teks — dan nilai kembaliannya fmt::Result yang menyatakan apakah penulisan berhasil. Nama method dan bentuk parameternya tidak boleh kita ubah; trait yang menentukan.
  3. Baris 5-9 (match self { ... }): pencocokan pola atas variant — kompiler kembali memastikan ketiga jenis kegagalan tertangani. Variant OutOfRange(usia) mengikat angka konteks ke variabel usia untuk disisipkan ke pesan.
  4. write!(f, ...): sepupu println! yang menulis ke formatter f alih-alih ke layar, lengkap dengan dukungan placeholder {}. Nilai kembalian fmt::Result menjadi nilai kembalian method secara implisit — tidak perlu titik koma di baris match.

Satu paket lengkapnya kini bisa kita uji:

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

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

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

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

text
Usia 150 tahun ditolak, batas maksimal 120

Mari kita amati pola pada pencocokannya:

  1. Baris 4 (Err(AgeError::OutOfRange(batas))): pola bertingkat — Err dibuka lebih dulu, lalu variant OutOfRange dibuka lagi untuk mengikat isi konteksnya ke batas. Angka 150 kini menjadi nilai biasa yang dipakai program, bukan sekadar potongan teks pesan.
  2. Baris 5 (Err(error)): pola penampung untuk dua variant sisanya; variabel error memegang seluruh nilai AgeError dan mencetaknya lewat Display. Tanpa pola ini, kompilator menolak match karena Empty dan NotNumber belum 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.