Fungsi di dunia nyata jarang berdiri sendiri: cek_pendaftaran memanggil parse_usia, yang memanggil parse; kegagalan di lapisan terdalam harus menempuh perjalanan naik sampai ke main sebelum ada yang berwenang memutuskan. Penyerahan error dari fungsi dalam ke fungsi pemanggilnya disebut propagation, dan pola match-nya sudah kita kenal: bila Err, kembalikan error itu segera. Pola tersebut benar, tetapi boros — lima baris upacara untuk satu baris niat, berulang di setiap lapisan. Rust mengganti seluruh ritual itu dengan satu karakter: operator tanya ?. Pada chapter ini kita akan membandingkan kedua bentuk, membongkar cara kerja ? di balik layar, lalu memakainya untuk membangun rantai pemanggilan yang error-nya mengalir naik dengan tenang.

Masalah Propagasi Error #

Kita mulai dari bentuk manual yang jujur. Fungsi berikut membaca usia dari teks; satu versi menuliskan propagasi dengan match, satu versi lagi memakai ?:

rust
fn parse_usia_manual(input: &str) -> Result<u32, std::num::ParseIntError> {
    let hasil_parse = input.parse::<u32>();
    let usia = match hasil_parse {
        Ok(nilai) => nilai,
        Err(error) => return Err(error),
    };
    Ok(usia)
}

fn parse_usia(input: &str) -> Result<u32, std::num::ParseIntError> {
    let usia = input.parse::<u32>()?;
    Ok(usia)
}

fn main() {
    println!("Manual : {:?}", parse_usia_manual("34"));
    println!("Ringkas: {:?}", parse_usia("34"));
    println!("Ringkas: {:?}", parse_usia("abc"));
}

Output yang Dihasilkan:

text
Manual : Ok(34)
Ringkas: Ok(34)
Ringkas: Err(ParseIntError { kind: InvalidDigit })

Mari kita bedah kode di atas:

  1. Baris 1-9 (fn parse_usia_manual): versi manual membungkus hasil parse dalam match; cabang Ok mengeluarkan nilainya, cabang Err memicu return Err(error) — error dikirim pulang ke pemanggil saat itu juga, melewati sisa fungsi.
  2. Baris 10-13 (fn parse_usia): versi ? mengerjakan hal yang persis sama dalam satu karakter. Ekspresi input.parse::<u32>()? berarti: “parse teks ini; kalau sukses, berikan nilainya ke usia; kalau gagal, hentikan fungsi ini dan kirim error-nya ke pemanggil”.
  3. Baris 16-18 (pemanggilan): hasil kedua fungsi identik untuk masukan yang sama — "34" menghasilkan Ok(34), "abc" menghasilkan Err — membuktikan ? setara penuh dengan match versi manual.

Yang hilang dari versi ringkas hanya upacara; keputusan tetap sama. Fungsi tetap berjanji Result pada tanda tangan, error tetap dikirim ke pemanggil, dan pemanggil tetap memutuskan langkah berikutnya.

Cara Kerja Operator ? di Balik Layar #

Secara mesin, ekspresi? diekspansi menjadi bentuk yang hampir sama dengan match manual kita — kurang lebih seperti ini:

rust
let nilai = match ekspresi {
    Ok(nilai) => nilai,
    Err(error) => return Err(error),
};

Tiga perilaku mengikuti bentuk tersebut. Pertama, ? hanya berlaku di fungsi yang tipe kembaliannya Result — cabang return Err(...) tidak akan masuk akal pada fungsi yang berjanji mengembalikan u32 polos; kompilator menolak pemakaian seperti itu dengan error tipe. Kedua, saat Err ditemui, sisa fungsi tidak dieksekusi sama sekali: baris-baris setelah ? melewati jalur kegagalan, sama seperti return. Ketiga, saat Ok ditemui, isi nilainya keluar dari ekspresi dan tipe usia menjadi u32 — bungkus Result sudah terbuka secara otomatis.

Propagasi Berlapis di Rantai Pemanggilan #

Kekuatan ? paling terasa ketika kegagalan harus menembus beberapa lapis fungsi. Program berikut memvalidasi pendaftaran kursus: cek_pendaftaran memanggil parse_usia, dan main memanggil cek_pendaftaran:

rust
fn parse_usia(input: &str) -> Result<u32, std::num::ParseIntError> {
    let usia = input.parse::<u32>()?;
    Ok(usia)
}

fn cek_pendaftaran(input: &str) -> Result<String, std::num::ParseIntError> {
    let usia = parse_usia(input)?;

    if usia >= 17 {
        Ok(String::from("Pendaftaran diterima"))
    } else {
        Ok(String::from("Pendaftaran ditolak: usia di bawah 17 tahun"))
    }
}

fn main() {
    for input in ["21", "15", "abc"] {
        match cek_pendaftaran(input) {
            Ok(pesan) => println!("Input '{}' -> {}", input, pesan),
            Err(error) => println!("Input '{}' -> gagal diproses: {}", input, error),
        }
    }
}

Output yang Dihasilkan:

text
Input '21' -> Pendaftaran diterima
Input '15' -> Pendaftaran ditolak: usia di bawah 17 tahun
Input 'abc' -> gagal diproses: invalid digit found in string

Mari kita telaah kode di atas:

  1. Baris 2 (let usia = input.parse::<u32>()?;): lapisan pertama ?. Teks "abc" gagal di sini dan langsung dikirim keluar dari parse_usia sebagai Err(ParseIntError).
  2. Baris 7 (let usia = parse_usia(input)?;): lapisan kedua ?. Pemanggilan parse_usia menghasilkan Err, jadi ? menghentikan cek_pendaftaran — blok if usia >= 17 di bawahnya tidak pernah dieksekusi untuk teks "abc" — dan error yang sama diteruskan ke main.
  3. Baris 9-13 (if usia >= 17 { ... }): logika domain yang bersih tanpa satu pun cabang error; seluruh kegagalan sudah selesai di baris bertanda ?. Jalur sukses menghasilkan Ok(String) berisi pesan keputusan.
  4. Baris 17-21 (match cek_pendaftaran(input)): main adalah pemanggil terakhir yang akhirnya mengambil keputusan: cabang Ok mencetak pesan hasil, cabang Err mencetak laporan kegagalan. Untuk masukan "abc", pesan yang tercetak berasal dari ParseIntError yang menempuh dua lapis fungsi tanpa pernah disentuh.

Perjalanan error untuk teks "abc" dapat kita petakan:

Diagram di atas menunjukkan inti propagation: error dari lapisan terdalam berpindah tangan naik, lapis demi lapis, sampai tiba di titik yang berwenang memutuskan — pada contoh kita, main yang memilih menampilkan pesan. ? tidak memilih jalur kegagalan; ? hanya memastikan perjalanan naiknya sependek mungkin.

Ringkasan #

Operator tanya ? memampatkan pola propagasi match-return Err menjadi satu karakter: Ok terbuka dan nilainya dipakai, Err menghentikan fungsi saat itu juga dan dikirim ke pemanggil. Ekspansi mekanismenya identik dengan match manual, sehingga tidak ada perilaku tersembunyi. Pada rantai berlapis, error dari lapisan terdalam melompat keluar melewati semua lapisan ? hingga tiba di pemanggil pengambil keputusan. Satu titik yang belum kita sentuh: fungsi main sendiri selama ini selalu memakai match di ujung rantai — padahal main juga bisa mengembalikan Result dan memakai ?. Chapter berikutnya membuka kemungkinan tersebut dengan Box<dyn Error>.