Kapan Harus panic! dan Kapan Nggak
Terus gimana caranya kita mutusin kapan harus manggil panic! dan kapan harus
balikin Result? Pas kode panic, nggak ada cara buat pulih (recover). Kita
bisa aja manggil panic! buat situasi error apa pun, mau ada cara buat pulih
atau nggak, tapi kalau gitu kita jadi ngambil keputusan atas nama kode pemanggil
kalau situasinya emang nggak bisa dipulihin. Pas kita milih buat balikin nilai
Result, kita ngasih opsi ke kode pemanggil. Kode pemanggil bisa milih buat
nyoba pulih dengan cara yang pas buat situasinya, atau dia bisa mutusin kalau
nilai Err di kasus ini emang nggak bisa dipulihin, jadi dia bisa manggil
panic! dan ngerubah error recoverable kita jadi error unrecoverable.
Makanya, balikin Result itu pilihan default yang bagus pas kita lagi
mendefinisikan fungsi yang mungkin aja gagal.
Di situasi-situasi kayak ngasih contoh, kode prototipe, sama nulis test, lebih
pantes buat nulis kode yang bakal panic daripada balikin Result. Yuk kita
eksplor alasannya, terus bahas situasi-situasi di mana compiler nggak bisa
tau kalau kegagalan itu mustahil terjadi, tapi kita sebagai manusia tau. Bab ini
bakal ditutup pake beberapa panduan umum (guidelines) soal gimana cara mutusin
apakah harus panic di kode library atau nggak.
Contoh-contoh, Kode Prototipe, sama Tests
Pas kita lagi nulis contoh buat ngejelasin suatu konsep, masukin kode penanganan
error yang kuat (robust) malah bisa bikin contohnya jadi kurang jelas. Di dalam
contoh-contoh, udah dimaklumin kalau pemanggilan ke method kayak unwrap yang
bisa panic itu dimaksudkan sebagai placeholder (tempat pengganti) buat
gimana kita maunya aplikasi kita nanganin error, yang mana bisa beda-beda
tergantung dari apa yang lagi dilakuin sama sisa kode kita.
Sama halnya, method unwrap sama expect itu praktis sekali pas lagi
prototyping (bikin prototipe), sebelum kita siap mutusin gimana cara nanganin
error. Mereka ninggalin tanda yang jelas di kode kita buat pas kita udah siap
bikin program kita jadi lebih kuat (robust).
Kalau pemanggilan method gagal di dalem sebuah test, kita pasti mau seluruh
test-nya ikutan gagal, biarpun method itu bukan fungsionalitas yang lagi dites.
Karena panic! adalah cara sebuah test ditandain gagal, manggil unwrap atau
expect adalah hal yang bener-bener seharusnya dilakuin.
Kasus di mana Kita Punya Lebih Banyak Informasi daripada Compiler
Bakal pantes juga buat manggil expect pas kita punya logika lain yang mastiin
kalau Result-nya bakal punya nilai Ok, tapi logikanya bukan sesuatu yang
dipahamin sama compiler. Kita bakal tetep punya nilai Result yang harus
ditanganin: operasi apa pun yang lagi kita panggil secara umum tetep punya
kemungkinan buat gagal, biarpun itu mustahil terjadi secara logika di situasi
spesifik kita. Kalau kita bisa mastiin dengan nge-cek kodenya secara manual
kalau kita nggak bakal pernah dapet varian Err, itu sah-sah aja buat manggil
expect dan dokumentasiin alesan kenapa kita yakin kita nggak bakal pernah dapet
varian Err di dalem teks argumennya. Ini contohnya:
fn main() {
use std::net::IpAddr;
let home: IpAddr = "127.0.0.1"
.parse()
.expect("Hardcoded IP address should be valid");
}
Kita bikin instance IpAddr dengan nge-parse (mengurai) sebuah hardcoded
string. Kita bisa liat kalau 127.0.0.1 itu alamat IP yang valid, jadi sah-sah
aja buat pake expect di sini. Tapi, punya hardcoded string yang valid nggak
ngubah tipe kembalian dari method parse: kita tetep dapet nilai Result, dan
compiler tetep bakal nyuruh kita nanganin Result-nya seolah-olah varian Err
itu mungkin terjadi karena compiler nggak cukup pinter buat ngeliat kalau
string ini bakal selalu jadi alamat IP yang valid. Kalau string alamat IP-nya
dateng dari user bukannya di-hardcode ke program dan makanya emang punya
kemungkinan gagal, kita pastinya mau nanganin Result-nya dengan cara yang
lebih kuat (robust) sebagai gantinya. Nyebutin asumsi kalau alamat IP ini di-
hardcode bakal ngingetin kita buat ngubah expect jadi kode penanganan error
yang lebih baik kalau, di masa depan, kita harus ngedapetin alamat IP dari
sumber lain.
Panduan buat Error Handling
Sangat disaranin buat bikin kode kita panic pas ada kemungkinan kode kita bisa berakhir di keadaan yang buruk (bad state). Di konteks ini, bad state adalah pas ada asumsi, jaminan, kontrak, atau invarian (aturan yang harus selalu benar) yang dilanggar, misalnya pas nilai yang nggak valid, nilai yang saling bertentangan, atau nilai yang ilang dimasukin ke kode kita—ditambah satu atau lebih dari hal- hal berikut:
- Bad state itu adalah sesuatu yang nggak diduga-duga, beda sama sesuatu yang kemungkinan bakal sesekali kejadian, kayak user masukin data pake format yang salah.
- Kode kita setelah titik ini harus ngandelin kalau dia nggak lagi di bad state itu, bukannya nge-cek masalah itu di tiap langkahnya.
- Nggak ada cara yang bagus buat nge-encode (nyimpen) informasi ini ke tipe- tipe yang kita pake. Kita bakal bahas contoh dari apa yang kita maksud di “Meng-encode Keadaan dan Perilaku sebagai Tipe” di Bab 18.
Kalau seseorang manggil kode kita terus ngasih nilai-nilai yang nggak masuk akal,
paling bener sih balikin sebuah error kalau bisa biar user dari library-nya
bisa mutusin apa yang mau mereka lakuin di kasus itu. Tapi, di kasus di mana
lanjut jalan bisa ngebahayain keamanan atau ngerusak, pilihan terbaik mungkin
adalah manggil panic! terus ngingetin orang yang pake library kita soal bug
di kode mereka biar mereka bisa benerin pas masa development (pengembangan).
Sama juga, panic! itu sering kali pas kalau kita lagi manggil kode eksternal
yang ada di luar kendali kita terus dia balikin invalid state yang nggak bisa
kita benerin.
Tapi, pas kegagalan emang udah di-ekspektasi, lebih pantes buat balikin sebuah
Result daripada manggil panic!. Contohnya kayak sebuah parser yang dikasih
data yang cacat formatnya (malformed) atau sebuah HTTP request yang balikin
status yang ngindikasikan kalau kita udah kena rate limit (batas batas frekuensi
permintaan). Di kasus-kasus ini, balikin Result nunjukin kalau kegagalan
adalah kemungkinan yang di-ekspektasi yang harus diputusin sama kode pemanggil
gimana cara nanganinnya.
Pas kode kita ngejalanin operasi yang bisa ngebahayain user kalau dipanggil
pake nilai-nilai yang nggak valid, kode kita harusnya nge-verifikasi kalau nilai-
nilainya valid dulu terus panic kalau ternyata nggak valid. Ini sebagian besar
karena alasan keamanan (safety): nyoba beroperasi pada data yang nggak valid
bisa nge-ekspos kode kita ke celah keamanan (vulnerabilities). Ini alasan utama
kenapa standard library bakal manggil panic! kalau kita nyoba akses memori
di luar batas (out-of-bounds): nyoba akses memori yang bukan milik struktur
data saat ini adalah masalah keamanan yang umum sekali. Fungsi-fungsi sering
kali punya contracts (kontrak): perilaku mereka cuma dijamin kalau inputnya
menuhin syarat tertentu. Panic pas kontrak dilanggar itu masuk akal karena
pelanggaran kontrak selalu ngindikasikan ada bug di pihak pemanggil (caller-side),
dan ini bukan jenis error yang kita mau kode pemanggil harus tangani secara
eksplisit. Malah, nggak ada cara yang masuk akal buat kode pemanggil buat bisa
pulih; si programmer yang bikin kode pemanggil harus benerin kodenya. Kontrak
buat sebuah fungsi, terutama pas ada pelanggaran yang bakal nyebabin panic,
harusnya dijelasin di dokumentasi API buat fungsi itu.
Tapi, punya sangat banyak pengecekan error di semua fungsi kita bakal panjang
sekali (verbose) dan nyebelin. Untungnya, kita bisa pake sistem tipe (type system)
Rust (dan karena itu dapet pengecekan tipe yang dilakuin sama compiler) buat
ngelakuin banyak pengecekan buat kita. Kalau fungsi kita nerima tipe tertentu
sebagai parameternya, kita bisa lanjut sama logika kode kita dengan tenang
karena tau compiler udah mastiin kalau kita punya nilai yang valid. Misalnya,
kalau kita nerima sebuah tipe bukannya sebuah Option, program kita berharap
dapet sesuatu bukannya nggak ada apa-apa. Kode kita terus nggak perlu
nanganin dua kasus buat varian Some sama None: dia cuma bakal punya satu
kasus di mana dia pasti dapet sebuah nilai. Kode yang nyoba masukin nggak ada
apa-apa ke fungsi kita bahkan nggak bakal bisa di-compile, jadi fungsi kita
nggak perlu nge-cek kasus itu pas runtime. Contoh lain adalah pake tipe integer
unsigned (nggak ada tanda minus) kayak u32, yang mastiin kalau parameternya
nggak bakal pernah negatif.
Bikin Tipe Kustom Buat Validasi
Yuk kita bawa ide pake sistem tipe Rust buat mastiin kita punya nilai yang valid satu langkah lebih jauh terus liat cara bikin tipe kustom buat validasi. Inget game tebak angka di Bab 2 di mana kode kita minta user buat nebak angka antara 1 sampe 100. Kita nggak pernah mevalidasi kalau tebakan user bener-bener ada di antara angka-angka itu sebelum kita bandingin sama angka rahasia kita; kita cuma mevalidasi kalau tebakannya itu positif. Di kasus ini, konsekuensinya nggak terlalu parah sih: output “Ketinggian” atau “Kerendahan” kita bakal tetep bener. Tapi ini bakal jadi peningkatan yang berguna buat mandu user ke arah tebakan yang valid dan punya perilaku yang beda pas user nebak angka di luar rentang versus pas user ngetik huruf, misalnya.
Salah satu cara buat ngelakuin ini adalah dengan nge-parse (mengurai) tebakannya
sebagai sebuah i32 bukannya cuma u32 buat ngebolehin angka yang potensial
negatif, terus nambahin pengecekan apakah angkanya ada di dalem rentang, kayak
gini:
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
// --snip--
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
if guess < 1 || guess > 100 {
println!("The secret number will be between 1 and 100.");
continue;
}
match guess.cmp(&secret_number) {
// --snip--
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
Ekspresi if nge-cek apakah nilai kita ada di luar rentang, ngasih tau user
soal masalahnya, terus manggil continue buat mulai iterasi loop berikutnya
dan minta tebakan lain. Setelah ekspresi if, kita bisa lanjut sama perbandingan
antara guess sama angka rahasianya dengan tenang karena tau guess pasti di
antara 1 sampe 100.
Tapi, ini bukan solusi yang ideal: kalau bener-bener kritis sekali (absolutely critical) kalau programnya cuma beroperasi pada nilai di antara 1 sampe 100, dan program itu punya banyak fungsi dengan persyaratan ini, punya pengecekan kayak gini di tiap fungsi bakal ngebosenin dan repetitif sekali (dan mungkin ngaruh ke performa juga).
Sebagai gantinya, kita bisa bikin tipe baru di dalem modul yang didedikasikan
khusus dan naruh validasinya di dalem sebuah fungsi buat bikin instance dari
tipe itu bukannya ngulangin validasinya di mana-mana. Dengan gitu, bakal aman
buat fungsi-fungsi buat pake tipe baru ini di signature mereka dan pake nilai
yang mereka terima dengan pede. Listing 9-13 nunjukin salah satu cara buat
mendefinisikan tipe Guess yang cuma bakal bikin instance dari Guess kalau
fungsi new nerima nilai di antara 1 sampe 100.
#![allow(unused)]
fn main() {
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
pub fn value(&self) -> i32 {
self.value
}
}
}
Guess yang cuma bakal lanjut kalau nilainya di antara 1 sampe 100Perhatiin ya kalau kode ini di src/guessing_game.rs bergantung sama penambahan
deklarasi modul mod guessing_game; di src/lib.rs yang belum kita tunjukin
di sini. Di dalem file modul baru ini, kita mendefinisikan sebuah struct namanya
Guess yang punya field namanya value yang nampung sebuah i32. Di sinilah
angkanya bakal disimpan.
Terus kita mengimplementasikan sebuah fungsi associated namanya new pada
Guess yang bertugas bikin instance-instance dari nilai Guess. Fungsi new
didefinisikan buat punya satu parameter namanya value dari tipe i32 dan
balikin sebuah Guess. Kode di body fungsi new ngetes value buat mastiin
kalau dia ada di antara 1 sampe 100. Kalau value nggak lolos tes ini, kita
manggil panic!, yang bakal ngingetin programmer yang nulis kode pemanggil
kalau mereka punya bug yang harus dibenerin, karena bikin sebuah Guess
dengan value di luar rentang ini bakal melanggar kontrak yang diandelin sama
Guess::new. Kondisi-kondisi di mana Guess::new mungkin bakal panic
harusnya didiskusikan di dokumentasi API yang ngadep public (public-facing API);
kita bakal ngebahas konvensi dokumentasi buat nunjukin kemungkinan panic! di
dokumentasi API yang kita bikin di Bab 14. Kalau value lolos tes, kita bikin
Guess baru dengan field value-nya di-set ke parameter value terus balikin
Guess-nya.
Selanjutnya, kita mengimplementasikan sebuah method namanya value yang minjem
(borrows) self, nggak punya parameter lain apa pun, dan balikin sebuah i32.
Tipe method kayak gini kadang disebut getter karena tujuannya adalah buat dapet
beberapa data dari field-nya terus balikin datanya. Method public ini dibutuhin
karena field value dari struct Guess itu private. Ini penting sekali biar
field value tetep private biar kode yang pake struct Guess nggak dibolehin
nge-set value secara langsung: kode di luar modul guessing_game harus pake
fungsi Guess::new buat bikin instance dari Guess, dan dengan gitu ngejamin
nggak ada cara buat sebuah Guess buat punya value yang belum dicek sama
kondisi di fungsi Guess::new.
Fungsi yang punya parameter atau balikin cuma angka di antara 1 sampe 100
kemudian bisa mendeklarasikan di signature-nya kalau dia nerima atau balikin
sebuah Guess bukannya sebuah i32 dan nggak perlu ngelakuin pengecekan
tambahan apa pun di body-nya.
Ringkasan
Fitur error-handling di Rust didesain buat ngebantu kita nulis kode yang
lebih kuat (robust). Macro panic! nandain kalau program kita ada di keadaan
yang dia nggak bisa tanganin dan ngasih kita cara buat nyuruh prosesnya buat
berhenti bukannya nyoba lanjut pake nilai yang nggak valid atau salah. Enum
Result pake sistem tipe Rust buat ngindikasikan kalau operasi bisa aja gagal
dengan cara yang kode kita bisa pulih (recover) darinya. Kita bisa pake Result
buat ngasih tau kode yang manggil kode kita kalau dia harus nanganin potensi
sukses atau gagal juga. Pake panic! sama Result di situasi yang pas bakal
bikin kode kita lebih bisa diandelin pas ngadepin masalah yang nggak bisa
dihindarin.
Sekarang setelah kita liat cara yang kepake sekali di mana standard library
pake generik bareng enum Option sama Result, kita bakal bahas gimana cara
kerja generik (generics) dan gimana kita bisa pakenya di kode kita.