Lewati ke konten

Error Kustom di Rust

Di halaman Result, setiap error hanyalah sebuah String. Itu tidak masalah untuk contoh kecil, tapi mulai bermasalah begitu sebuah fungsi bisa gagal dengan lebih dari satu cara. String tidak bisa di-match per jenis kegagalan, jadi pemanggil tidak bisa tahu kegagalan mana yang terjadi tanpa membaca isi teksnya sendiri.

fn withdraw(balance: f64, amount: f64) -> Result<f64, String> {
if amount > balance {
return Err(String::from("saldo tidak cukup"));
}
if amount < 0.0 {
return Err(String::from("jumlah tidak boleh negatif"));
}
Ok(balance - amount)
}

Kalau pemanggil ingin bereaksi berbeda untuk “saldo tidak cukup” dan “jumlah negatif”, satu-satunya pilihan adalah membandingkan string error-nya. Cara itu rapuh, dan langsung rusak begitu kamu mengubah kata-kata pesannya.

Sebuah enum menyelesaikan masalah ini: setiap jenis kegagalan menjadi variannya sendiri, yang bisa langsung di-match oleh pemanggil:

enum WithdrawError {
InsufficientFunds,
NegativeAmount,
}
fn withdraw(balance: f64, amount: f64) -> Result<f64, WithdrawError> {
if amount < 0.0 {
return Err(WithdrawError::NegativeAmount);
}
if amount > balance {
return Err(WithdrawError::InsufficientFunds);
}
Ok(balance - amount)
}
fn main() {
match withdraw(100.0, 150.0) {
Ok(remaining) => println!("Saldo baru: {remaining}"),
Err(WithdrawError::InsufficientFunds) => println!("Uangnya tidak cukup"),
Err(WithdrawError::NegativeAmount) => println!("Tidak bisa menarik jumlah negatif"),
}
}

Output:

Uangnya tidak cukup
▶ Coba sendiri

Sekarang compiler memastikan setiap jenis kegagalan ditangani. Jaminannya sama dengan yang selalu diberikan match pada enum, hanya saja kini diterapkan pada error.

Mengimplementasikan Display untuk pesan error yang layak

Section titled “Mengimplementasikan Display untuk pesan error yang layak”

Coba cetak WithdrawError dengan {} sekarang, dan kodenya tidak akan bisa dikompilasi: Rust tidak tahu cara mengubahnya menjadi teks. Mengimplementasikan trait Display menyelesaikan hal itu. Trait akan punya halamannya sendiri sebentar lagi (Trait dan Trait umum). Untuk sekarang, kamu bisa menyalin pola ini apa adanya:

use std::fmt;
enum WithdrawError {
InsufficientFunds,
NegativeAmount,
}
impl fmt::Display for WithdrawError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
WithdrawError::InsufficientFunds => write!(f, "saldo tidak cukup"),
WithdrawError::NegativeAmount => write!(f, "jumlah tidak boleh negatif"),
}
}
}
fn main() {
let error = WithdrawError::InsufficientFunds;
println!("Error: {error}");
}

Output:

Error: saldo tidak cukup
▶ Coba sendiri

Sekarang WithdrawError berperilaku seperti error lain yang ingin kamu tampilkan ke user atau ke log, sambil tetap bisa di-match sepenuhnya oleh kode yang perlu bereaksi terhadap kasus tertentu.

Menulis Display dan kode boilerplate lain untuk tipe error kustom secara manual lama-lama jadi berulang di proyek sungguhan. Crate thiserror membuatkan sebagian besar kode itu untukmu lewat #[derive], dan anyhow populer untuk kode aplikasi yang hanya perlu meneruskan error tanpa mendefinisikan tipe untuk masing-masing. Bagus untuk tahu bahwa keduanya ada, tapi kamu tidak membutuhkannya untuk mempelajari dasar-dasarnya. Kalau sudah siap, halaman anyhow dan thiserror menunjukkan keduanya beraksi, dan kapan memilih yang mana.

Definisikan enum LoginError dengan dua varian, EmptyUsername dan PasswordTooShort, lalu implementasikan Display untuknya. Kemudian tulis fn check_login(user: &str, password: &str) -> Result<String, LoginError> yang mengembalikan Ok berisi pesan sambutan, atau error yang sesuai. Password harus minimal 8 karakter. Petunjuk: .is_empty() dan .len() pada &str sudah cukup untuk pemeriksaannya.

Lihat solusi
use std::fmt;
enum LoginError {
EmptyUsername,
PasswordTooShort,
}
impl fmt::Display for LoginError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
LoginError::EmptyUsername => write!(f, "username tidak boleh kosong"),
LoginError::PasswordTooShort => write!(f, "password minimal 8 karakter"),
}
}
}
fn check_login(user: &str, password: &str) -> Result<String, LoginError> {
if user.is_empty() {
return Err(LoginError::EmptyUsername);
}
if password.len() < 8 {
return Err(LoginError::PasswordTooShort);
}
Ok(format!("Selamat datang, {user}!"))
}
fn main() {
let attempts = [("ana", "correct-horse"), ("", "secret123"), ("ben", "short")];
for (user, password) in attempts {
match check_login(user, password) {
Ok(message) => println!("{message}"),
Err(e) => println!("Error: {e}"),
}
}
}

Output:

Selamat datang, ana!
Error: username tidak boleh kosong
Error: password minimal 8 karakter
▶ Coba sendiri

Password contohnya sengaja dibiarkan dalam bahasa Inggris, karena hasilnya bergantung pada panjang teks itu.

Tulis fn parse_percent(input: &str) -> Result<u32, PercentError>, di mana PercentError punya dua varian yang menyimpan data: NotANumber(String) (teks yang salah) dan TooLarge(u32) (angka yang lebih dari 100). Buat pesan Display-nya memuat detail tersebut. Petunjuk: varian enum bisa menyimpan nilai, seperti di halaman Enum. Di dalam fmt, lakukan match dengan PercentError::TooLarge(n) => ... untuk mengambil nilainya.

Lihat solusi
use std::fmt;
enum PercentError {
NotANumber(String),
TooLarge(u32),
}
impl fmt::Display for PercentError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PercentError::NotANumber(text) => write!(f, "'{text}' bukan angka"),
PercentError::TooLarge(n) => write!(f, "{n} lebih dari 100"),
}
}
}
fn parse_percent(input: &str) -> Result<u32, PercentError> {
match input.parse::<u32>() {
Ok(n) => {
if n > 100 {
Err(PercentError::TooLarge(n))
} else {
Ok(n)
}
}
Err(_) => Err(PercentError::NotANumber(input.to_string())),
}
}
fn main() {
for input in ["75", "setengah", "250"] {
match parse_percent(input) {
Ok(n) => println!("{n}%"),
Err(e) => println!("Error: {e}"),
}
}
}

Output:

75%
Error: 'setengah' bukan angka
Error: 250 lebih dari 100
▶ Coba sendiri
  • Error berupa String cukup untuk contoh kecil, tapi pemanggil tidak bisa melakukan match per jenis kegagalan.
  • Definisikan enum dengan satu varian untuk setiap jenis kegagalan, supaya pemanggil bisa melakukan match pada error tertentu.
  • Implementasikan Display (impl fmt::Display for YourError) supaya tipe error-nya bisa dicetak dengan {}.
  • Di proyek yang lebih besar, crate seperti thiserror dan anyhow menghilangkan sebagian besar kode boilerplate.

Kuis singkat

1. Kenapa tipe error String tidak lagi cukup begitu sebuah fungsi bisa gagal dengan lebih dari satu cara?

2. Apa yang membuat pemanggil withdraw bisa bereaksi berbeda terhadap InsufficientFunds dan NegativeAmount?

3. Apa yang bisa kamu lakukan setelah mengimplementasikan Display untuk tipe error kustom?

4. Crate seperti thiserror dan anyhow membantu dalam hal apa?

5. Kenapa membandingkan string pesan error mentah dianggap rapuh?

Skor: 0 / 5