Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

Filename: src/main.rs
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.

Filename: src/guessing_game.rs
#![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
    }
}
}
Listing 9-13: Sebuah tipe Guess yang cuma bakal lanjut kalau nilainya di antara 1 sampe 100

Perhatiin 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.