Eksepsi #
Rust tidak punya try-catch. Ini bukan kelupaan — ini keputusan desain yang disengaja. Di bahasa dengan exception, fungsi bisa melempar error kapan saja tanpa deklarasi di signature-nya, membuat pemanggil tidak tahu apakah perlu menangani error atau tidak. Rust menolak ketidakpastian ini: setiap fungsi yang bisa gagal harus mendeklarasikannya secara eksplisit di return type-nya, dan pemanggil wajib menangani hasilnya. Hasilnya adalah kode yang lebih verbose di satu sisi, tapi dengan jaminan kuat: jika program berhasil dikompilasi, tidak ada error yang bisa lolos tak tertangani secara diam-diam. Artikel ini membahas dua jalur error handling Rust — Result untuk error yang bisa dipulihkan dan panic! untuk yang tidak — beserta pola-pola idiomatis yang membuat kode error handling tetap ringkas dan expressif.
Dua Kategori Error di Rust #
Rust membagi error menjadi dua kategori yang berbeda secara fundamental, dan menangani keduanya dengan mekanisme yang berbeda pula:
flowchart TD
E[Error di Rust]
E --> R["Recoverable\nBisa dipulihkan\nProgram bisa lanjut"]
E --> U["Unrecoverable\nBug atau kondisi tak terduga\nProgram harus berhenti"]
R --> R1["Result<T, E>\nNilai Ok atau Err\nPemanggil harus handle"]
R --> R2["Option<T>\nNilai Some atau None\nUntuk 'tidak ada nilai'"]
U --> U1["panic!\nUnwind stack\nCetak pesan dan berhenti"]
U --> U2["assert! / assert_eq!\nunreachable! / todo!\nVariasi panic yang bermakna"]Aturan praktisnya sederhana: gunakan Result untuk error yang wajar terjadi dalam penggunaan normal (file tidak ditemukan, input tidak valid, koneksi gagal), dan panic! untuk kondisi yang seharusnya tidak mungkin terjadi (index di luar batas, invariant yang dilanggar, bug logika).
Result<T, E> — Error yang Bisa Dipulihkan
#
Result adalah enum bawaan dengan dua variant:
// Definisi di standard library
enum Result<T, E> {
Ok(T), // operasi berhasil, membawa nilai tipe T
Err(E), // operasi gagal, membawa error tipe E
}
Setiap fungsi yang bisa gagal mengembalikan Result. Compiler memaksa pemanggil menangani keduanya:
use std::fs;
use std::num::ParseIntError;
// Fungsi yang mungkin gagal — deklarasi di signature
fn baca_file(path: &str) -> Result<String, std::io::Error> {
fs::read_to_string(path)
}
fn parse_angka(s: &str) -> Result<i32, ParseIntError> {
s.trim().parse::<i32>()
}
fn main() {
// Penanganan dengan match — paling eksplisit
match baca_file("config.txt") {
Ok(isi) => println!("Isi file: {}", isi),
Err(e) => println!("Gagal baca file: {}", e),
}
// Penanganan dengan if let — jika hanya peduli Ok
if let Ok(n) = parse_angka("42") {
println!("Angka: {}", n);
}
// unwrap_or — nilai default jika Err
let n = parse_angka("bukan angka").unwrap_or(0);
println!("Dengan default: {}", n);
// unwrap_or_else — hitung nilai default secara lazy
let n2 = parse_angka("xyz").unwrap_or_else(|e| {
eprintln!("Parse gagal: {}", e);
-1
});
println!("Dengan fallback: {}", n2);
}
Method Penting pada Result
#
fn main() {
let ok: Result<i32, &str> = Ok(42);
let err: Result<i32, &str> = Err("gagal");
// Cek status
println!("{} {}", ok.is_ok(), ok.is_err()); // true false
println!("{} {}", err.is_ok(), err.is_err()); // false true
// Transformasi nilai Ok dengan map
let doubled = ok.map(|n| n * 2);
println!("{:?}", doubled); // Ok(84)
// Transformasi error dengan map_err
let dengan_pesan = err.map_err(|e| format!("Error: {}", e));
println!("{:?}", dengan_pesan); // Err("Error: gagal")
// and_then — rangkai operasi yang bisa gagal
let hasil = parse_angka("10")
.and_then(|n| {
if n > 0 { Ok(n * 2) }
else { Err("harus positif".parse::<i32>().unwrap_err()) }
});
// ok() — konversi Result ke Option (buang informasi error)
let sebagai_option: Option<i32> = ok.ok();
println!("{:?}", sebagai_option); // Some(42)
// ANTI-PATTERN: unwrap() di kode produksi tanpa pemeriksaan
// err.unwrap(); // panic: called `Result::unwrap()` on an `Err` value
// Gunakan expect() dengan pesan yang informatif saat prototyping:
// err.expect("Operasi yang seharusnya selalu berhasil");
}
fn parse_angka(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse()
}
Operator ? — Propagasi Error Otomatis
#
Operator ? adalah cara paling idiomatis menyebarkan error ke atas call stack. Ia mengekstrak nilai Ok jika berhasil, atau langsung return Err(...) jika gagal — satu karakter menggantikan seluruh blok match:
use std::fs;
use std::io;
use std::num::ParseIntError;
// Tanpa operator ? — verbose
fn baca_dan_parse_verbose(path: &str) -> Result<i32, io::Error> {
let isi = match fs::read_to_string(path) {
Ok(s) => s,
Err(e) => return Err(e),
};
// tidak bisa langsung return ParseIntError karena tipenya berbeda
// harus konversi manual
Ok(42) // disederhanakan
}
// Dengan operator ? — bersih dan linear
fn baca_dan_parse(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let isi = fs::read_to_string(path)?; // propagasi io::Error
let angka: i32 = isi.trim().parse()?; // propagasi ParseIntError
Ok(angka * 2)
}
fn main() {
match baca_dan_parse("angka.txt") {
Ok(n) => println!("Hasil: {}", n),
Err(e) => eprintln!("Error: {}", e),
}
}
? dalam main()
#
Sejak Rust 2018, main() bisa mengembalikan Result, memungkinkan penggunaan ? langsung di dalamnya:
use std::fs;
use std::io;
fn main() -> Result<(), io::Error> {
let isi = fs::read_to_string("config.txt")?;
println!("Konfigurasi:\n{}", isi);
Ok(()) // main berhasil
}
Jika Err dikembalikan dari main, program mencetak error dan keluar dengan kode non-zero — persis seperti error handling CLI yang benar.
Tipe Error Kustom #
Untuk library atau aplikasi yang lebih besar, mendefinisikan tipe error sendiri memungkinkan error yang lebih deskriptif dan lebih mudah ditangani oleh pemanggil:
use std::fmt;
use std::num::ParseIntError;
// Tipe error kustom dengan enum
#[derive(Debug)]
enum AppError {
IoError(std::io::Error),
ParseError(ParseIntError),
Validasi(String),
}
// Implementasikan Display untuk pesan yang ramah pengguna
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::IoError(e) => write!(f, "Error I/O: {}", e),
AppError::ParseError(e) => write!(f, "Error parsing: {}", e),
AppError::Validasi(msg) => write!(f, "Validasi gagal: {}", msg),
}
}
}
// Implementasikan std::error::Error agar bisa digunakan sebagai Box<dyn Error>
impl std::error::Error for AppError {}
// From trait memungkinkan konversi otomatis via operator ?
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self {
AppError::IoError(e)
}
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::ParseError(e)
}
}
fn proses_file(path: &str) -> Result<i32, AppError> {
let isi = std::fs::read_to_string(path)?; // io::Error → AppError::IoError otomatis via From
let n: i32 = isi.trim().parse()?; // ParseIntError → AppError::ParseError otomatis
if n < 0 {
return Err(AppError::Validasi(format!("Nilai {} harus positif", n)));
}
Ok(n * 2)
}
fn main() {
match proses_file("data.txt") {
Ok(hasil) => println!("Hasil: {}", hasil),
Err(AppError::IoError(e)) => eprintln!("File error: {}", e),
Err(AppError::ParseError(e)) => eprintln!("Parse error: {}", e),
Err(AppError::Validasi(msg)) => eprintln!("Validasi: {}", msg),
}
}
Box<dyn Error> untuk Error Dinamis
#
Ketika tidak perlu membedakan tipe error spesifik di pemanggil, Box<dyn std::error::Error> adalah cara cepat untuk menerima semua jenis error:
use std::error::Error;
// Terima semua tipe error — praktis untuk script dan binary
fn baca_konfigurasi(path: &str) -> Result<(String, u16), Box<dyn Error>> {
let isi = std::fs::read_to_string(path)?; // io::Error
let mut baris = isi.lines();
let host = baris.next()
.ok_or("Baris host tidak ditemukan")? // &str sebagai error
.to_string();
let port: u16 = baris.next()
.ok_or("Baris port tidak ditemukan")?
.trim()
.parse()?; // ParseIntError
Ok((host, port))
}
fn main() -> Result<(), Box<dyn Error>> {
let (host, port) = baca_konfigurasi("server.conf")?;
println!("Server: {}:{}", host, port);
Ok(())
}
Untuk library yang akan digunakan orang lain, definisikan tipe error kustom agar pemanggil bisa menangani kasus error yang berbeda secara terpisah. Box<dyn Error> lebih cocok untuk binary atau prototipe di mana detail tipe error tidak penting untuk pemanggil.Option<T> — Ketiadaan Nilai Bukan Error
#
Option digunakan ketika tidak adanya nilai adalah kondisi yang wajar dan bukan tanda adanya masalah — berbeda dari Result yang membawa informasi tentang apa yang salah:
fn cari_pengguna(id: u32) -> Option<String> {
match id {
1 => Some(String::from("Budi")),
2 => Some(String::from("Sari")),
_ => None, // tidak ditemukan — bukan error, hanya tidak ada
}
}
fn ambil_pertama(v: &[i32]) -> Option<&i32> {
v.first() // None jika kosong, bukan error
}
fn main() {
// Berbagai cara menangani Option
let nama = cari_pengguna(1);
// match — paling eksplisit
match nama {
Some(n) => println!("Ditemukan: {}", n),
None => println!("Tidak ada"),
}
// unwrap_or
let nama = cari_pengguna(99).unwrap_or(String::from("Anonim"));
println!("{}", nama);
// map — transformasi jika Some
let panjang = cari_pengguna(2).map(|n| n.len());
println!("{:?}", panjang); // Some(4)
// and_then — rangkai operasi Option
let hasil = cari_pengguna(1)
.and_then(|nama| if nama.len() > 3 { Some(nama) } else { None });
println!("{:?}", hasil);
// ? bekerja di fungsi yang return Option juga
fn panjang_nama(id: u32) -> Option<usize> {
let nama = cari_pengguna(id)?; // return None jika tidak ada
Some(nama.len())
}
println!("{:?}", panjang_nama(1)); // Some(4)
println!("{:?}", panjang_nama(99)); // None
}
Konversi antara Option dan Result
#
fn main() {
// Option → Result dengan ok_or / ok_or_else
let angka: Option<i32> = Some(42);
let sebagai_result: Result<i32, &str> = angka.ok_or("tidak ada nilai");
println!("{:?}", sebagai_result); // Ok(42)
let none: Option<i32> = None;
let err: Result<i32, &str> = none.ok_or("nilai tidak ada");
println!("{:?}", err); // Err("nilai tidak ada")
// Result → Option dengan ok() dan err()
let result: Result<i32, &str> = Ok(42);
let opt: Option<i32> = result.ok(); // buang info error
println!("{:?}", opt); // Some(42)
let result_err: Result<i32, &str> = Err("gagal");
let err_opt: Option<&str> = result_err.err(); // ambil nilai error
println!("{:?}", err_opt); // Some("gagal")
}
panic! — Error yang Tidak Bisa Dipulihkan
#
panic! menghentikan thread saat ini, membersihkan stack (unwind), dan mencetak pesan error beserta backtrace. Gunakan hanya untuk kondisi yang seharusnya tidak mungkin terjadi dalam penggunaan yang benar:
fn akses_aman(data: &[i32], indeks: usize) -> i32 {
// ANTI-PATTERN: panic untuk error yang bisa ditangani
// if indeks >= data.len() {
// panic!("Indeks {} di luar batas", indeks);
// }
// data[indeks]
// BENAR: kembalikan Option atau Result
data.get(indeks).copied().unwrap_or_else(|| {
panic!("Bug: indeks {} seharusnya selalu valid di titik ini", indeks)
})
}
// Contoh sah penggunaan panic — invariant yang tidak boleh dilanggar
fn buat_koneksi_pool(ukuran: usize) {
assert!(ukuran > 0, "Ukuran pool harus lebih dari 0, dapat: {}", ukuran);
assert!(ukuran <= 100, "Ukuran pool maksimal 100, dapat: {}", ukuran);
// ... lanjutkan inisialisasi
}
fn main() {
// panic! langsung
// panic!("Ini error yang tidak bisa dipulihkan");
// assert! — panic jika kondisi false
let x = 5;
assert!(x > 0, "x harus positif");
assert_eq!(x, 5, "x harus bernilai 5");
assert_ne!(x, 0, "x tidak boleh nol");
// unreachable! — tandai kode yang seharusnya tidak pernah dicapai
let nilai = 2;
match nilai {
1 => println!("satu"),
2 => println!("dua"),
3 => println!("tiga"),
_ => unreachable!("nilai hanya bisa 1, 2, atau 3"),
}
// todo! dan unimplemented! — placeholder selama pengembangan
// todo!("Implementasikan fungsi ini nanti");
// unimplemented!("Belum diimplementasikan");
}
panic! vs Result — Kapan Memilih
#
Gunakan panic! jika:
✓ Kondisi yang terjadi adalah bug yang tidak seharusnya mungkin
✓ Invariant yang dijamin oleh kontrak API dilanggar
✓ Inisialisasi gagal dan program tidak bisa melanjutkan sama sekali
✓ Kode prototype/test (gunakan .unwrap() dan .expect())
Gunakan Result jika:
✓ Error adalah kemungkinan yang wajar dalam penggunaan normal
✓ File tidak ditemukan, koneksi timeout, input tidak valid
✓ Pemanggil perlu tahu apa yang salah dan mungkin bisa menanganinya
✓ Kode library — jangan paksa pengguna library untuk panic
Pola Idiomatis Error Handling #
Rangkaian Operasi dengan ?
#
use std::io;
use std::fs;
#[derive(Debug)]
struct Konfigurasi {
host: String,
port: u16,
debug: bool,
}
fn muat_konfigurasi(path: &str) -> Result<Konfigurasi, Box<dyn std::error::Error>> {
let isi = fs::read_to_string(path)?;
let mut lines = isi.lines();
let host = lines.next()
.ok_or("Field 'host' tidak ditemukan")?
.trim()
.to_string();
let port = lines.next()
.ok_or("Field 'port' tidak ditemukan")?
.trim()
.parse::<u16>()?;
let debug = lines.next()
.ok_or("Field 'debug' tidak ditemukan")?
.trim()
.parse::<bool>()?;
Ok(Konfigurasi { host, port, debug })
}
Collect Vec<Result> menjadi Result<Vec>
#
fn main() {
let input = vec!["1", "2", "3", "4", "5"];
// Collect semua ke Result — gagal di elemen pertama yang error
let angka: Result<Vec<i32>, _> = input.iter()
.map(|s| s.parse::<i32>())
.collect();
println!("{:?}", angka); // Ok([1, 2, 3, 4, 5])
let input_rusak = vec!["1", "dua", "3"];
let gagal: Result<Vec<i32>, _> = input_rusak.iter()
.map(|s| s.parse::<i32>())
.collect();
println!("{:?}", gagal); // Err(ParseIntError)
// Lewati yang error — hanya ambil yang berhasil
let hanya_valid: Vec<i32> = input_rusak.iter()
.filter_map(|s| s.parse::<i32>().ok())
.collect();
println!("{:?}", hanya_valid); // [1, 3]
}
Pattern map + and_then untuk Pipeline
#
fn validasi_usia(input: &str) -> Result<u8, String> {
input.trim()
.parse::<u8>()
.map_err(|_| format!("'{}' bukan angka valid", input))
.and_then(|usia| {
if usia >= 18 {
Ok(usia)
} else {
Err(format!("Usia {} kurang dari minimum 18", usia))
}
})
}
fn main() {
println!("{:?}", validasi_usia("25")); // Ok(25)
println!("{:?}", validasi_usia("15")); // Err("Usia 15 kurang dari minimum 18")
println!("{:?}", validasi_usia("abc")); // Err("'abc' bukan angka valid")
println!("{:?}", validasi_usia("999")); // Err("'999' bukan angka valid") — overflow u8
}
Ringkasan #
- Rust tidak punya exception — sebagai gantinya, semua error yang bisa dipulihkan direpresentasikan sebagai nilai
Result<T, E>di return type fungsi. Compiler memaksa pemanggil menanganinya.Resultuntuk error yang wajar,panic!untuk bug — file tidak ditemukan →Result; akses indeks yang dijamin valid ternyata di luar batas →panic!.- Operator
?menyebarkan error otomatis — mengekstrakOkatau langsung returnErrke pemanggil. Bisa digunakan di fungsi yang returnResultatauOption.?dimain()memungkinkan — kembalikanResult<(), E>darimainagar bisa menggunakan?langsung tanpaunwrap.- Tipe error kustom via enum — definisikan
enum AppErrordengan variant per kategori, implementasikanDisplay,Error, danFromuntuk konversi otomatis via?.Box<dyn Error>untuk prototyping — menerima semua tipe error, berguna di binary dan script tapi kurang informatif untuk library.Fromtrait mengaktifkan konversi otomatis — jikaimpl From<io::Error> for AppErrorada, makaio::Errorotomatis dikonversi saat menggunakan?.Optionbukan subsetResult— keduanya punya peran berbeda:Optionuntuk “nilai mungkin tidak ada”,Resultuntuk “operasi mungkin gagal dengan info error”. Konversi antara keduanya via.ok(),.ok_or(), dan.ok_or_else().collect::<Result<Vec<_>, _>>()— mengumpulkan iteratorResultmenjadiResult<Vec>, gagal di elemen pertama yangErr. Atau gunakanfilter_mapuntuk melanjutkan dan lewati error.