Eksepsi

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.
  • Result untuk error yang wajar, panic! untuk bug — file tidak ditemukan → Result; akses indeks yang dijamin valid ternyata di luar batas → panic!.
  • Operator ? menyebarkan error otomatis — mengekstrak Ok atau langsung return Err ke pemanggil. Bisa digunakan di fungsi yang return Result atau Option.
  • ? di main() memungkinkan — kembalikan Result<(), E> dari main agar bisa menggunakan ? langsung tanpa unwrap.
  • Tipe error kustom via enum — definisikan enum AppError dengan variant per kategori, implementasikan Display, Error, dan From untuk konversi otomatis via ?.
  • Box<dyn Error> untuk prototyping — menerima semua tipe error, berguna di binary dan script tapi kurang informatif untuk library.
  • From trait mengaktifkan konversi otomatis — jika impl From<io::Error> for AppError ada, maka io::Error otomatis dikonversi saat menggunakan ?.
  • Option bukan subset Result — keduanya punya peran berbeda: Option untuk “nilai mungkin tidak ada”, Result untuk “operasi mungkin gagal dengan info error”. Konversi antara keduanya via .ok(), .ok_or(), dan .ok_or_else().
  • collect::<Result<Vec<_>, _>>() — mengumpulkan iterator Result menjadi Result<Vec>, gagal di elemen pertama yang Err. Atau gunakan filter_map untuk melanjutkan dan lewati error.

← Sebelumnya: Trait   Berikutnya: List →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact