Episode ini membedah error handling di Rust: tipe Result dan Option sebagai nilai error-safe, match untuk menangani hasil, operator ? untuk propagasi error yang ringkas, serta pembentukan custom error dengan thiserror dan anyhow untuk aplikasi production.

Hampir semua program di dunia nyata bisa gagal: file tidak ditemukan, koneksi terputus, atau format data salah. Di bahasa lain, error sering ditangani lewat exception yang melompat keluar dari alur normal. Rust mengambil pendekatan berbeda: error adalah nilai, direpresentasikan dengan tipe Result dan Option.
Episode 4 membedah kedua tipe ini, pola match untuk menanganinya, operator ? untuk propagasi error yang ringkas, serta dua crate paling populer untuk custom error: thiserror untuk library dan anyhow untuk aplikasi. Setelah episode ini, kalian akan membaca pesan error Rust dengan kepala dingin.
Result<T, E> menyatakan operasi yang bisa berhasil atau gagal: varian Ok(T) membawa nilai sukses, varian Err(E) membawa error. Option<T> menyatakan nilai yang mungkin tidak ada: Some(T) atau None. Keduanya adalah enum biasa — tidak ada sihir, hanya disiplin tipe.
cat > src/main.rs <<'EOF'
use std::fs::File;
fn main() {
let hasil = File::open("tidak-ada.txt");
match hasil {
Ok(file) => println!("file terbuka: {:?}", file),
Err(e) => println!("gagal membuka file: {}", e),
}
}
EOF
cargo runFile::open mengembalikan Result<File, io::Error>. match hasil memaksa kalian menangani kedua kasus. Di sinilah kekuatan Rust: kemungkinan gagal tidak bisa diabaikan begitu saja.
Option menjawab pertanyaan "apakah nilainya ada?". Contoh paling umum: mencari elemen di koleksi.
cat > src/main.rs <<'EOF'
fn main() {
let angka = vec![10, 20, 30];
let pertama = angka.first();
let kosong: Vec<i32> = Vec::new();
let dari_kosong = kosong.first();
println!("pertama: {:?}", pertama);
println!("dari_kosong: {:?}", dari_kosong);
}
EOF
cargo runvec.first() mengembalikan Option<&i32>: Some jika ada elemen, None jika vektor kosong. Dengan menggabungkan Option dan Result, Rust menangani ketidakhadiran nilai tanpa null.
Pola match pada episode 3 adalah cara dasar menangani Result dan Option. Untuk logika yang hanya peduli pada kasus sukses, kombinator seperti unwrap_or, map, dan ok_or membuat kode lebih padat:
cat > src/main.rs <<'EOF'
fn main() {
let input = "42";
let angka: Result<i32, _> = input.parse();
let nilai = angka.unwrap_or(0);
let opsional = Some(7);
let tambah = opsional.map(|n| n + 1);
println!("nilai: {}, tambah: {:?}", nilai, tambah);
}
EOF
cargo rununwrap_or(0) memakai nilai fallback jika Err. map mengubah isi Some tanpa menyentuh None. Hindari unwrap dan expect pada kode production karena keduanya memicu panic saat Err atau None.
Menulis match untuk setiap panggilan yang bisa gagal membuat kode berantakan. Operator ? memendekkannya: jika Err, fungsi mengembalikan error tersebut langsung; jika Ok, isinya diteruskan ke variabel.
cat > src/main.rs <<'EOF'
use std::fs;
use std::io;
fn baca_panjang(path: &str) -> Result<usize, io::Error> {
let konten = fs::read_to_string(path)?;
Ok(konten.len())
}
fn main() -> Result<(), io::Error> {
let panjang = baca_panjang("Cargo.toml")?;
println!("panjang Cargo.toml: {}", panjang);
Ok(())
}
EOF
cargo runfs::read_to_string(path)? mengembalikan error ke pemanggil jika gagal. Perhatikan fn main() -> Result<(), io::Error>: main sendiri kini bisa mengembalikan error, dan runtime mencetaknya jika terjadi.
Operator ? melakukan konversi otomatis lewat trait From: jika fungsi mengembalikan io::Error tetapi sumbernya ParseIntError, Rust memanggil konversi yang sesuai. Inilah dasar ekosistem error yang saling terhubung.
thiserror menghasilkan implementasi Display dan std::error::Error dari definisi enum — sempurna untuk library yang harus memberi error terstruktur:
cat > src/main.rs <<'EOF'
use std::fs;
#[derive(thiserror::Error, Debug)]
enum AppError {
#[error("file tidak ditemukan: {0}")]
NotFound(String),
#[error("gagal membaca: {0}")]
Io(#[from] std::io::Error),
}
fn baca(path: &str) -> Result<String, AppError> {
let konten = fs::read_to_string(path)?;
Ok(konten)
}
fn main() -> Result<(), AppError> {
let isi = baca("Cargo.toml")?;
println!("{}", isi.lines().next().unwrap_or_default());
Ok(())
}
EOF
cargo run#[error("file tidak ditemukan: {0}")] mendefinisikan pesan, dan #[from] menambahkan konversi otomatis. cargo add thiserror menambahkan dependency ke Cargo.toml. Dengan #[from], operator ? langsung mengonversi io::Error menjadi AppError.
Untuk binary dan aplikasi yang tidak butuh error terstruktur, anyhow menyediakan Result<T, anyhow::Error> dengan konteks string:
cat > src/main.rs <<'EOF'
use anyhow::{Context, Result};
fn main() -> Result<()> {
let data = std::fs::read_to_string("config.toml")
.context("gagal membaca config.toml")?;
println!("{}", data.lines().count());
Ok(())
}
EOF
cargo runcontext menambahkan pesan kontekstual ke error. Kombinasi idiomatis di industri: thiserror untuk library, anyhow untuk aplikasi dan binary.
Inti yang harus dibawa pulang:
Result<T, E> untuk operasi yang bisa gagal; Option<T> untuk nilai yang bisa absen.match memaksa kedua kasus ditangani; kombinator seperti unwrap_or dan map memadatkan logika.? mempropagasi error dan mengonversi tipe lewat trait From.fn main() -> Result<(), E> membiarkan error bocor ke runtime.thiserror untuk custom error terstruktur di library.anyhow dengan context untuk error aplikasi yang kaya informasi.Di episode 5 selanjutnya kita akan membahas struct, enum, trait, dan generic — definisi data dengan struct dan tuple struct, enum sebagai algebraic data types, trait sebagai interface dengan default impl, serta generic constraints untuk kode yang reusable. Kalian akan mulai membangun tipe data yang mencerminkan domain bisnis.