Option & Result

Option & Result #

Bahasa lain memiliki dua cara klasik untuk menangani kegagalan dan ketiadaan nilai: nilai sentinel seperti null atau -1, dan exceptions yang bisa dilempar kapan saja dari fungsi manapun tanpa tertulis di tanda tangannya. Keduanya punya masalah yang sama — compiler tidak bisa memaksa programmer menanganinya. NullPointerException di Java dan nil pointer dereference di Go adalah bukti bahwa pendekatan ini memang tidak cukup. Rust memilih jalan berbeda: ketiadaan nilai dan kemungkinan kegagalan adalah bagian dari sistem tipe, bukan pengecualian. Option<T> merepresentasikan nilai yang mungkin ada atau tidak ada. Result<T, E> merepresentasikan operasi yang mungkin berhasil atau gagal. Keduanya harus ditangani secara eksplisit — jika tidak, kode tidak akan dikompilasi.

Mengapa Option dan Result, Bukan Null dan Exception #

Sebelum melihat cara menggunakan keduanya, penting untuk memahami mengapa Rust merancangnya sebagai tipe biasa, bukan sebagai mekanisme khusus bahasa.

Saat sebuah fungsi mengembalikan Option<String>, tanda tangannya secara eksplisit mengkomunikasikan: “fungsi ini mungkin tidak menghasilkan nilai.” Caller tidak bisa mengabaikan kemungkinan ini — compiler memaksa mereka menangani kasus None. Berbeda dengan bahasa yang mengembalikan String tapi bisa mengembalikan null secara diam-diam.

Saat sebuah fungsi mengembalikan Result<Data, IoError>, tanda tangannya mengkomunikasikan dua hal sekaligus: tipe nilai sukses (Data) dan tipe error yang mungkin terjadi (IoError). Tidak ada error tersembunyi yang bisa muncul dari fungsi yang tidak mencantumkannya di tanda tangan.

// Bahasa lain: fungsi yang bisa mengembalikan null atau melempar exception
// String cariPengguna(int id)  ← bisa null, bisa throw, tidak jelas dari tanda tangan

// Rust: tanda tangan mencerminkan perilaku secara akurat
fn cari_pengguna(id: u32) -> Option<String> { ... }         // mungkin tidak ada
fn baca_file(path: &str) -> Result<String, io::Error> { ... } // mungkin gagal
flowchart TD
    A[Fungsi dipanggil] --> B{Operasi berhasil?}
    B -- Ya, ada nilai --> C["Option::Some(T)"]
    B -- Tidak ada nilai --> D["Option::None"]
    B -- Ya, sukses --> E["Result::Ok(T)"]
    B -- Gagal dengan error --> F["Result::Err(E)"]

    C --> G[Compiler paksa tangani kedua kasus]
    D --> G
    E --> H[Compiler paksa tangani kedua kasus]
    F --> H

    style C fill:#e8f5e9
    style D fill:#ffebee
    style E fill:#e8f5e9
    style F fill:#ffebee

Option<T> — Nilai yang Mungkin Tidak Ada #

Option<T> adalah enum dengan dua varian: Some(T) saat nilai ada, dan None saat tidak ada. Gunakan Option saat ketiadaan nilai adalah kondisi normal yang bukan merupakan error — misalnya mencari elemen di koleksi, mengambil elemen pertama dari slice, atau field opsional dalam struct.

fn main() {
    // Membuat Option
    let ada: Option<i32> = Some(42);
    let tidak_ada: Option<i32> = None;

    // Koleksi mengembalikan Option saat elemen mungkin tidak ada
    let angka = vec![1, 2, 3, 4, 5];
    let pertama: Option<&i32> = angka.first();       // Some(&1)
    let dari_indeks: Option<&i32> = angka.get(10);   // None — indeks di luar batas
    let dicari: Option<&i32> = angka.iter().find(|&&x| x > 3);  // Some(&4)

    // HashMap mengembalikan Option saat kunci tidak ada
    use std::collections::HashMap;
    let mut skor: HashMap<&str, i32> = HashMap::new();
    skor.insert("Alice", 100);

    let skor_alice: Option<&i32> = skor.get("Alice");  // Some(&100)
    let skor_bob: Option<&i32> = skor.get("Bob");      // None
}

Pattern Matching pada Option #

match adalah cara paling eksplisit dan fleksibel untuk menangani Option. Gunakan saat kamu perlu logika berbeda untuk Some dan None, atau saat kamu perlu binding ke nilai di dalam Some.

fn deskripsi_skor(skor: Option<i32>) -> String {
    match skor {
        Some(s) if s >= 90 => format!("Sangat baik: {}", s),
        Some(s) if s >= 70 => format!("Baik: {}", s),
        Some(s) => format!("Perlu ditingkatkan: {}", s),
        None => String::from("Belum ada skor"),
    }
}

fn main() {
    println!("{}", deskripsi_skor(Some(95)));  // "Sangat baik: 95"
    println!("{}", deskripsi_skor(Some(75)));  // "Baik: 75"
    println!("{}", deskripsi_skor(None));      // "Belum ada skor"

    // if let — lebih ringkas saat hanya perlu menangani Some
    let nilai = Some(42);
    if let Some(v) = nilai {
        println!("Nilai: {}", v);
    }

    // while let — iterasi sampai None
    let mut stack = vec![1, 2, 3];
    while let Some(top) = stack.pop() {
        println!("Popped: {}", top);
    }
}

Method pada Option #

Option memiliki banyak method yang memungkinkan transformasi dan penanganan tanpa match eksplisit. Ini membuat kode lebih ringkas dan komposabel.

fn main() {
    let ada: Option<i32> = Some(10);
    let tidak_ada: Option<i32> = None;

    // unwrap — ambil nilai, panic jika None
    // ANTI-PATTERN: unwrap di kode produksi tanpa yakin tidak None
    let nilai = ada.unwrap();  // 10, tapi panic jika tidak_ada.unwrap()

    // unwrap_or — nilai default jika None
    let a = ada.unwrap_or(0);          // 10
    let b = tidak_ada.unwrap_or(0);    // 0

    // unwrap_or_else — default dari closure (lazy evaluation)
    let c = tidak_ada.unwrap_or_else(|| {
        println!("Menghitung default...");
        42
    });

    // unwrap_or_default — default dari trait Default
    let d: i32 = tidak_ada.unwrap_or_default();  // 0 (default i32)

    // map — transformasi nilai di dalam Some, None tetap None
    let doubled = ada.map(|v| v * 2);        // Some(20)
    let doubled_none = tidak_ada.map(|v| v * 2); // None

    // map_or — map dengan nilai default jika None
    let e = ada.map_or(0, |v| v * 2);        // 20
    let f = tidak_ada.map_or(0, |v| v * 2);  // 0

    // and_then — chaining operasi yang masing-masing mengembalikan Option
    let hasil = ada
        .and_then(|v| if v > 5 { Some(v * 2) } else { None })
        .and_then(|v| if v < 100 { Some(v) } else { None });
    println!("{:?}", hasil);  // Some(20)

    // filter — kembalikan None jika nilai tidak memenuhi kondisi
    let genap = ada.filter(|v| v % 2 == 0);  // Some(10)
    let ganjil = ada.filter(|v| v % 2 != 0); // None

    // or dan or_else — fallback ke Option lain jika None
    let g = tidak_ada.or(Some(99));   // Some(99)
    let h = ada.or(Some(99));         // Some(10) — ada tidak berubah

    // is_some dan is_none — pengecekan tanpa consuming
    println!("{}", ada.is_some());        // true
    println!("{}", tidak_ada.is_none());  // true

    // as_ref — pinjam isi tanpa consuming Option
    let s: Option<String> = Some(String::from("halo"));
    let panjang = s.as_ref().map(|v| v.len());  // s masih valid setelah ini
    println!("{:?}", s);  // Some("halo") — tidak ter-move
}
flowchart LR
    A["Option<T>"] --> B["map(f)"]
    A --> C["and_then(f)"]
    A --> D["filter(pred)"]
    A --> E["or(opt)"]

    B --> B1["Option<U> — transformasi nilai"]
    C --> C1["Option<U> — chaining Option-returning fn"]
    D --> D1["Option<T> — None jika kondisi tidak terpenuhi"]
    E --> E1["Option<T> — fallback jika None"]

    style A fill:#e3f2fd
    style B1 fill:#e8f5e9
    style C1 fill:#e8f5e9
    style D1 fill:#e8f5e9
    style E1 fill:#e8f5e9

Result<T, E> — Operasi yang Bisa Gagal #

Result<T, E> adalah enum dengan dua varian: Ok(T) saat operasi berhasil, dan Err(E) saat gagal dengan error bertipe E. Gunakan Result saat kegagalan adalah sesuatu yang perlu dikomunikasikan dan ditangani — operasi I/O, parsing, network request, validasi input.

use std::num::ParseIntError;
use std::io;

// Fungsi yang bisa gagal mengembalikan Result
fn parse_angka_positif(s: &str) -> Result<u32, String> {
    let angka: i64 = s.trim().parse().map_err(|_| format!("'{}' bukan angka valid", s))?;
    if angka < 0 {
        return Err(format!("Angka harus positif, dapat: {}", angka));
    }
    Ok(angka as u32)
}

fn main() {
    // Membuat Result secara manual
    let sukses: Result<i32, String> = Ok(42);
    let gagal: Result<i32, String> = Err(String::from("sesuatu gagal"));

    // Operasi standar library yang mengembalikan Result
    let parsed: Result<i32, ParseIntError> = "42".parse();
    let file_content: Result<String, io::Error> = std::fs::read_to_string("file.txt");

    // Pattern matching
    match parse_angka_positif("123") {
        Ok(n) => println!("Berhasil: {}", n),
        Err(e) => println!("Gagal: {}", e),
    }

    match parse_angka_positif("abc") {
        Ok(n) => println!("Berhasil: {}", n),
        Err(e) => println!("Gagal: {}", e),  // "Gagal: 'abc' bukan angka valid"
    }
}

Method pada Result #

Result memiliki method yang sangat mirip dengan Option, ditambah method yang khusus untuk menangani error.

fn main() {
    let ok: Result<i32, String> = Ok(10);
    let err: Result<i32, String> = Err(String::from("error"));

    // unwrap dan expect — panic saat Err
    // ANTI-PATTERN: unwrap tanpa konteks di kode produksi
    let nilai = ok.unwrap();  // 10

    // BENAR: expect memberikan pesan error yang informatif
    let nilai = ok.expect("Seharusnya tidak gagal di tahap ini");

    // unwrap_or, unwrap_or_else, unwrap_or_default
    let a = err.unwrap_or(0);
    let b = err.unwrap_or_else(|e| {
        eprintln!("Error ditangani: {}", e);
        -1
    });

    // map — transformasi Ok, Err tetap Err
    let doubled = ok.map(|v| v * 2);         // Ok(20)
    let doubled_err = err.map(|v| v * 2);    // Err("error")

    // map_err — transformasi Err, Ok tetap Ok
    let konversi = err.map_err(|e| format!("Wrapped: {}", e));
    // Err("Wrapped: error")

    // and_then — chaining operasi Result-returning
    let hasil = ok
        .and_then(|v| if v > 5 { Ok(v * 2) } else { Err(String::from("terlalu kecil")) })
        .and_then(|v| Ok(v + 1));
    println!("{:?}", hasil);  // Ok(21)

    // or dan or_else — fallback Result jika Err
    let fallback = err.or(Ok(99));   // Ok(99)
    let fallback2 = err.or_else(|e| {
        println!("Mencoba recovery dari: {}", e);
        Ok(0)
    });

    // is_ok dan is_err
    println!("{}", ok.is_ok());    // true
    println!("{}", err.is_err());  // true

    // ok() — konversi Result ke Option (Err menjadi None)
    let sebagai_option: Option<i32> = ok.ok();   // Some(10)
    let err_option: Option<i32> = err.ok();      // None

    // err() — ambil nilai Err sebagai Option
    let error_value: Option<String> = err.err(); // Some("error")
}

Operator ? — Propagasi Error yang Elegan #

Operator ? adalah fitur yang membuat error handling di Rust terasa ergonomis. Saat dipasang setelah ekspresi Result, ia melakukan dua hal: jika Ok, ia mengekstrak nilai di dalamnya. Jika Err, ia langsung mengembalikan Err dari fungsi yang sedang berjalan. Ini menghilangkan kebutuhan match berulang untuk setiap operasi yang bisa gagal.

use std::io;
use std::fs;
use std::num::ParseIntError;

// ANTI-PATTERN: match berulang untuk setiap operasi — verbose dan mengganggu alur
fn baca_angka_dari_file_verbose(path: &str) -> Result<i32, String> {
    let konten = match fs::read_to_string(path) {
        Ok(s) => s,
        Err(e) => return Err(e.to_string()),
    };
    let angka = match konten.trim().parse::<i32>() {
        Ok(n) => n,
        Err(e) => return Err(e.to_string()),
    };
    Ok(angka * 2)
}

// BENAR: operator ? membuat alur utama tetap terbaca
fn baca_angka_dari_file(path: &str) -> Result<i32, String> {
    let konten = fs::read_to_string(path).map_err(|e| e.to_string())?;
    let angka = konten.trim().parse::<i32>().map_err(|e| e.to_string())?;
    Ok(angka * 2)
}

? juga bekerja pada Option — jika None, fungsi langsung mengembalikan None:

fn nama_depan(nama_lengkap: &str) -> Option<&str> {
    let bagian: Vec<&str> = nama_lengkap.split_whitespace().collect();
    let pertama = bagian.first()?;  // None jika string kosong
    Some(pertama)
}

fn inisial(nama_lengkap: &str) -> Option<char> {
    let depan = nama_depan(nama_lengkap)?;  // propagasi None
    depan.chars().next()                     // karakter pertama
}
Operator ? hanya bisa digunakan di dalam fungsi yang mengembalikan Result atau Option. Menggunakannya di main() biasa akan menyebabkan compile error. Solusinya: ubah signature main menjadi fn main() -> Result<(), Box<dyn Error>>, atau tangani error secara eksplisit di main.
use std::error::Error;
use std::fs;

// main yang bisa menggunakan ?
fn main() -> Result<(), Box<dyn Error>> {
    let konten = fs::read_to_string("config.txt")?;
    let nilai: i32 = konten.trim().parse()?;
    println!("Nilai dari config: {}", nilai);
    Ok(())
}

Chaining — Komposisi Tanpa Nested Match #

Salah satu kekuatan Option dan Result adalah kemampuan dikomposisi secara fungsional. Daripada nested match yang membuat kode terasa seperti piramida, kamu bisa merantai operasi secara linear.

use std::collections::HashMap;

fn cari_email_pengguna(db: &HashMap<u32, HashMap<&str, &str>>, id: u32) -> Option<String> {
    // ANTI-PATTERN: nested match — susah dibaca dan di-maintain
    match db.get(&id) {
        Some(profil) => {
            match profil.get("email") {
                Some(email) => {
                    if email.contains('@') {
                        Some(email.to_uppercase())
                    } else {
                        None
                    }
                }
                None => None,
            }
        }
        None => None,
    }
}

// BENAR: chaining yang linear dan mudah dibaca
fn cari_email_pengguna(db: &HashMap<u32, HashMap<&str, &str>>, id: u32) -> Option<String> {
    db.get(&id)
        .and_then(|profil| profil.get("email"))
        .filter(|email| email.contains('@'))
        .map(|email| email.to_uppercase())
}

fn main() {
    let mut db = HashMap::new();
    let mut profil = HashMap::new();
    profil.insert("email", "[email protected]");
    profil.insert("nama", "Budi");
    db.insert(1u32, profil);

    println!("{:?}", cari_email_pengguna(&db, 1));    // Some("[email protected]")
    println!("{:?}", cari_email_pengguna(&db, 99));   // None — id tidak ada
}

Pola yang sama berlaku untuk Result:

use std::num::ParseIntError;

fn hitung_dari_input(a: &str, b: &str) -> Result<i32, String> {
    // Chaining Result — jika satu gagal, seluruh chain gagal
    let x: i32 = a.trim()
        .parse()
        .map_err(|_: ParseIntError| format!("'{}' bukan angka valid", a))?;

    let y: i32 = b.trim()
        .parse()
        .map_err(|_: ParseIntError| format!("'{}' bukan angka valid", b))?;

    if y == 0 {
        return Err(String::from("Pembagi tidak boleh nol"));
    }

    Ok(x / y)
}

fn main() {
    println!("{:?}", hitung_dari_input("10", "2"));    // Ok(5)
    println!("{:?}", hitung_dari_input("10", "0"));    // Err("Pembagi tidak boleh nol")
    println!("{:?}", hitung_dari_input("abc", "2"));   // Err("'abc' bukan angka valid")
}

Konversi Antara Option dan Result #

Dalam kode nyata, kamu sering perlu mengonversi antara Option dan Result — misalnya saat fungsi library mengembalikan Option tapi kamu bekerja di konteks yang mengharapkan Result.

fn main() {
    // Option → Result dengan ok_or dan ok_or_else
    let ada: Option<i32> = Some(42);
    let tidak_ada: Option<i32> = None;

    let r1: Result<i32, &str> = ada.ok_or("nilai tidak ada");      // Ok(42)
    let r2: Result<i32, &str> = tidak_ada.ok_or("nilai tidak ada"); // Err("nilai tidak ada")

    // ok_or_else — lazy, error dibuat hanya jika diperlukan
    let r3: Result<i32, String> = tidak_ada.ok_or_else(|| {
        format!("Nilai tidak ditemukan pada waktu {}", chrono_waktu())
    });

    // Result → Option dengan ok() dan err()
    let ok: Result<i32, &str> = Ok(42);
    let err: Result<i32, &str> = Err("gagal");

    let opt1: Option<i32> = ok.ok();    // Some(42)
    let opt2: Option<i32> = err.ok();   // None — error dibuang

    // Skenario nyata: mencari di HashMap, lalu proses hasilnya
    use std::collections::HashMap;

    let konfigurasi: HashMap<&str, &str> = [
        ("port", "8080"),
        ("timeout", "30"),
    ].into_iter().collect();

    fn ambil_port(config: &HashMap<&str, &str>) -> Result<u16, String> {
        config
            .get("port")
            .ok_or_else(|| String::from("Konfigurasi 'port' tidak ditemukan"))?
            .parse::<u16>()
            .map_err(|e| format!("Port tidak valid: {}", e))
    }

    match ambil_port(&konfigurasi) {
        Ok(port) => println!("Server berjalan di port {}", port),
        Err(e) => eprintln!("Error konfigurasi: {}", e),
    }
}

fn chrono_waktu() -> String { String::from("sekarang") }
flowchart TD
    A["Option<T>"] -- "ok_or(e)" --> B["Result<T, E>"]
    A -- "ok_or_else(|| e)" --> B
    B -- "ok()" --> C["Option<T>"]
    B -- "err()" --> D["Option<E>"]
    C -- "ok_or(...)" --> B

    style A fill:#e8f5e9
    style B fill:#e3f2fd
    style C fill:#e8f5e9
    style D fill:#fff3e0

Tipe Error Custom #

Untuk aplikasi yang lebih besar, kamu perlu mendefinisikan tipe error sendiri yang merepresentasikan semua kemungkinan kegagalan di domain aplikasimu.

use std::fmt;
use std::num::ParseIntError;
use std::io;

// Definisi error custom dengan enum
#[derive(Debug)]
enum AppError {
    Io(io::Error),
    Parse(ParseIntError),
    Validasi(String),
    TidakDitemukan { resource: String, id: u32 },
}

// Implementasi Display untuk pesan yang ramah pengguna
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "Error I/O: {}", e),
            AppError::Parse(e) => write!(f, "Error parsing: {}", e),
            AppError::Validasi(msg) => write!(f, "Error validasi: {}", msg),
            AppError::TidakDitemukan { resource, id } => {
                write!(f, "{} dengan id {} tidak ditemukan", resource, id)
            }
        }
    }
}

// Implementasi From untuk konversi otomatis dengan operator ?
impl From<io::Error> for AppError {
    fn from(e: io::Error) -> Self {
        AppError::Io(e)
    }
}

impl From<ParseIntError> for AppError {
    fn from(e: ParseIntError) -> Self {
        AppError::Parse(e)
    }
}

// Dengan implementasi From, operator ? bisa konversi error secara otomatis
fn proses_konfigurasi(path: &str) -> Result<u32, AppError> {
    let konten = std::fs::read_to_string(path)?;  // io::Error → AppError::Io otomatis
    let nilai: u32 = konten.trim().parse()?;       // ParseIntError → AppError::Parse otomatis

    if nilai == 0 {
        return Err(AppError::Validasi(String::from("Nilai tidak boleh nol")));
    }

    Ok(nilai)
}

fn main() {
    match proses_konfigurasi("config.txt") {
        Ok(nilai) => println!("Konfigurasi berhasil: {}", nilai),
        Err(AppError::Io(e)) => eprintln!("Gagal baca file: {}", e),
        Err(AppError::Parse(e)) => eprintln!("Format tidak valid: {}", e),
        Err(AppError::Validasi(msg)) => eprintln!("Validasi gagal: {}", msg),
        Err(e) => eprintln!("Error lain: {}", e),
    }
}

Menggunakan thiserror untuk Boilerplate yang Lebih Sedikit #

Crate thiserror menyederhanakan pembuatan tipe error custom dengan derive macro:

# Cargo.toml
[dependencies]
thiserror = "1"
use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("Error I/O: {0}")]
    Io(#[from] std::io::Error),

    #[error("Error parsing: {0}")]
    Parse(#[from] std::num::ParseIntError),

    #[error("Error validasi: {0}")]
    Validasi(String),

    #[error("{resource} dengan id {id} tidak ditemukan")]
    TidakDitemukan { resource: String, id: u32 },
}

// Implementasi From di-generate otomatis oleh #[from]
// Display di-generate dari atribut #[error("...")]
// Kode proses_konfigurasi di atas bisa digunakan langsung tanpa perubahan

Menggunakan anyhow untuk Prototyping Cepat #

Crate anyhow berguna saat kamu tidak peduli tipe error spesifik — misalnya di binary (bukan library) atau saat prototyping:

[dependencies]
anyhow = "1"
use anyhow::{Context, Result, bail, ensure};

fn baca_dan_proses(path: &str) -> Result<i32> {
    // Context menambahkan pesan konteks ke error yang ada
    let konten = std::fs::read_to_string(path)
        .with_context(|| format!("Gagal membaca file '{}'", path))?;

    let nilai: i32 = konten.trim().parse()
        .context("Isi file bukan angka yang valid")?;

    // bail! — shorthand untuk return Err(anyhow!(...))
    if nilai < 0 {
        bail!("Nilai harus positif, dapat: {}", nilai);
    }

    // ensure! — assert yang mengembalikan Err jika kondisi false
    ensure!(nilai < 1000, "Nilai {} melebihi batas maksimum 1000", nilai);

    Ok(nilai * 2)
}

fn main() -> Result<()> {
    let hasil = baca_dan_proses("input.txt")?;
    println!("Hasil: {}", hasil);
    Ok(())
}
Gunakan thiserror untuk library — tipe error yang eksplisit memungkinkan pengguna library melakukan pattern matching. Gunakan anyhow untuk binary atau application code — lebih ergonomis dan tidak membutuhkan definisi enum error yang detail.

Mengumpulkan Result dari Iterator #

Saat memproses koleksi dan setiap item bisa gagal, Rust menyediakan cara idiomatik untuk mengumpulkan hasilnya.

fn main() {
    let input = vec!["1", "2", "3", "4", "5"];
    let salah = vec!["1", "dua", "3", "empat", "5"];

    // collect() ke Result<Vec<T>, E> — gagal jika ada satu item yang Err
    let semua_ok: Result<Vec<i32>, _> = input.iter()
        .map(|s| s.parse::<i32>())
        .collect();
    println!("{:?}", semua_ok);  // Ok([1, 2, 3, 4, 5])

    let ada_error: Result<Vec<i32>, _> = salah.iter()
        .map(|s| s.parse::<i32>())
        .collect();
    println!("{:?}", ada_error);  // Err(ParseIntError { ... })

    // Pisahkan Ok dan Err — proses yang valid, abaikan yang tidak
    let (berhasil, gagal): (Vec<_>, Vec<_>) = salah.iter()
        .map(|s| s.parse::<i32>())
        .partition(Result::is_ok);

    let nilai: Vec<i32> = berhasil.into_iter().map(|r| r.unwrap()).collect();
    let error: Vec<_> = gagal.into_iter().map(|r| r.unwrap_err()).collect();

    println!("Berhasil: {:?}", nilai);  // [1, 3, 5]
    println!("Gagal: {:?}", error);     // [ParseIntError, ParseIntError]

    // filter_map — abaikan None/Err secara diam-diam
    let hanya_valid: Vec<i32> = salah.iter()
        .filter_map(|s| s.parse::<i32>().ok())
        .collect();
    println!("Hanya valid: {:?}", hanya_valid);  // [1, 3, 5]
}

Kapan Menggunakan Option vs Result #

Memilih antara Option dan Result bukan sekadar soal preferensi — ada perbedaan semantik yang harus dipahami.

Gunakan Option jika:
  ✓ Ketiadaan nilai adalah kondisi normal, bukan error
  ✓ Operasi pencarian yang mungkin tidak menemukan hasil
  ✓ Field opsional dalam struct
  ✓ Iterator yang bisa habis
  ✓ Nilai yang belum diinisialisasi

Gunakan Result jika:
  ✗ Kegagalan terjadi karena kondisi eksternal (I/O, network, parsing)
  ✗ Caller perlu tahu MENGAPA gagal, bukan hanya bahwa gagal
  ✗ Kegagalan adalah pengecualian dari alur normal
  ✗ Error perlu dilaporkan atau di-log
  ✗ Operasi yang membutuhkan resource dan bisa ditolak sistem

Contoh yang sering membingungkan:

// Mencari di HashMap — gunakan Option
// Tidak ada yang "salah" jika kunci tidak ada
fn cari_pengguna(db: &HashMap<u32, String>, id: u32) -> Option<&String> {
    db.get(&id)
}

// Membaca pengguna dari database — gunakan Result
// Bisa gagal karena koneksi database, query error, dll.
fn ambil_pengguna(id: u32) -> Result<Pengguna, DbError> {
    // koneksi database bisa gagal, query bisa gagal
    db.query("SELECT * FROM users WHERE id = ?", [id])
}

// Parsing nilai opsional dari konfigurasi
// Option jika field opsional, Result jika format harus valid
fn port_konfigurasi(config: &HashMap<&str, &str>) -> Result<Option<u16>, String> {
    match config.get("port") {
        None => Ok(None),  // tidak ada port = gunakan default
        Some(s) => s.parse::<u16>()
            .map(Some)
            .map_err(|_| format!("Port '{}' tidak valid", s)),
    }
}

Ringkasan #

  • Option<T> untuk ketiadaan, Result<T, E> untuk kegagalanOption saat tidak adanya nilai adalah kondisi normal; Result saat kegagalan terjadi karena kondisi eksternal yang perlu dilaporkan.
  • Hindari unwrap() di kode produksi — gunakan unwrap_or, unwrap_or_else, atau ? untuk penanganan yang aman. Simpan unwrap() untuk prototyping atau test.
  • Operator ? untuk propagasi error — jauh lebih bersih dari match berulang. Pastikan fungsi mengembalikan Result atau Option yang kompatibel, dan implementasikan From untuk konversi tipe error otomatis.
  • map dan and_then untuk chainingmap mengubah nilai di dalam tanpa mengubah Some/Ok vs None/Err; and_then untuk operasi yang sendirinya mengembalikan Option atau Result.
  • ok_or dan ok_or_else untuk konversi Option → Result — berguna saat bekerja di konteks yang mengharapkan Result tapi fungsi library mengembalikan Option.
  • Tipe error custom dengan thiserror — untuk library, definisikan enum error dengan varian eksplisit agar pengguna bisa melakukan pattern matching. Gunakan #[from] untuk konversi otomatis.
  • anyhow untuk binary dan application code — ergonomis, tidak membutuhkan definisi tipe error detail, dan mendukung konteks error yang kaya lewat .context().
  • collect::<Result<Vec<_>, _>>() — cara idiomatik untuk memproses koleksi yang masing-masing itemnya bisa gagal. Gagal cepat saat ada satu error, atau gunakan filter_map untuk abaikan error secara diam-diam.

← Sebelumnya: Math   Berikutnya: Iterators →

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