Error Pustaka vs Aplikasi

thiserror untuk pustaka dan anyhow untuk binary.

Daftar isi

Topik ini membahas perbedaan kebutuhan error antara pustaka dan aplikasi, serta dua crate yang menjadi kebiasaan umum komunitas Rust untuk masing-masing peran. Pembahasan mencakup bentuk error yang tepat bagi API publik, pelaporan error yang mudah dibaca pada binary, pola kerja thiserror dan anyhow yang hanya dapat dikompilasi lewat Cargo karena keduanya crate eksternal, serta rencana serupa yang dapat ditulis hanya dengan pustaka standar. Setelah membaca topik ini, error pada kode sendiri dapat ditempatkan sesuai konsumennya, dan program utuh dengan main yang mengembalikan Result dapat dibangun tanpa dependensi eksternal.

Dua kebutuhan yang berbeda#

Pustaka dan aplikasi menghadapi error dari dua arah yang berlawanan. Pustaka dipanggil oleh kode lain yang tidak dapat diramalkan saat pustaka ditulis, sehingga tipe errornya menjadi bagian dari API, dan pemanggil perlu mencocokkan jenis kegagalan dengan pola untuk merangkai pemulihan secara programatik. Aplikasi berdiri di ujung rantai sebagai binary yang dijalankan manusia, sehingga kebutuhannya adalah pesan yang menjelaskan apa yang sedang dicoba ketika kegagalan terjadi. Konsekuensinya, pustaka mengembalikan informasi terstruktur tanpa mencetak apa pun, sedangkan aplikasi mengumpulkan konteks di sepanjang rantai panggilan lalu mencetaknya sekali di titik paling luar.

Aspekpustakaaplikasi
konsumen errorkode lain melalui APIpengguna akhir dan log
bentuk yang diutamakanenum error yang dapat dicocokkansatu tipe error serba-bisa
konteks kegagalanfield terstruktur pada variantpesan berlapis sepanjang rantai
pencetakantidak pernah mencetak sendirisekali, di titik paling luar

thiserror untuk tipe error pustaka#

Crate thiserror menyediakan derive macro yang menuliskan bagian-bagian mekanis tipe error pustaka. Anotasi #[derive(Error)] mengimplementasikan trait std::error::Error, #[error("...")] menghasilkan implementasi Display dari teks yang ditulis, dan #[from] membuat implementasi From sehingga operator ? dapat mengonversi secara otomatis. Seluruh pekerjaan itu sebenarnya dapat ditulis dengan tangan sebagaimana bagian akhir topik ini, dan thiserror hanya mempersingkat penulisannya.

rust Contoh: tipe error pustaka dengan thiserror
// tidak-dijalankan
use std::num::ParseIntError;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ErrorPengaturan {
    #[error("berkas pengaturan tidak dapat dibaca")]
    Baca(#[from] std::io::Error),

    #[error("nilai {nama} bukan angka")]
    BukanAngka {
        nama: String,
        #[source]
        sumber: ParseIntError,
    },
}

Variant Baca memakai #[from] sehingga setiap operator ? atas std::io::Error di dalam fungsi yang mengembalikan ErrorPengaturan dikonversi otomatis ke variant tersebut. Variant BukanAngka membawa nama field bersama error sumbernya yang ditandai #[source], sehingga pemanggil dapat menelusuri rantai penyebab sampai ke ParseIntError asli. Teks pada #[error] menjadi pesan Display, dan penanda {nama} menyisipkan isi field ke dalam pesan. Contoh ini ditandai tidak-dijalankan karena thiserror adalah crate eksternal yang hanya dapat diunduh Cargo, bukan oleh rustc yang berdiri sendiri.

anyhow untuk error aplikasi#

Crate anyhow mengambil arah sebaliknya dengan menyediakan satu tipe error yang menerima error apa pun yang mengimplementasikan std::error::Error. Alias anyhow::Result<T> berarti Result<T, anyhow::Error>, dan metode context serta with_context menambahkan lapisan pesan pada titik propagasi. Rantai konteks inilah yang membuat laporan akhirnya dapat dibaca manusia.

rust Contoh: pelaporan error aplikasi dengan anyhow
// tidak-dijalankan
use anyhow::{Context, Result};

fn baca_batas(alamat: &str) -> Result<u32> {
    let teks = std::fs::read_to_string(alamat)
        .with_context(|| format!("gagal membaca {}", alamat))?;
    let nilai: u32 = teks
        .trim()
        .parse()
        .context("nilai batas bukan angka")?;
    Ok(nilai)
}

fn main() -> Result<()> {
    let batas = baca_batas("pengaturan.txt")?;
    println!("batas {}", batas);
    Ok(())
}

Ketika berkas tidak ada, program mencetak baris Error: gagal membaca pengaturan.txt yang diikuti bagian Caused by: berisi penyebab dari sistem operasi. Ketika berkas berisi teks yang salah format, konteks nilai batas bukan angka menjadi lapisan pertama dan invalid digit found in string dari ParseIntError menjadi lapisan di bawahnya. Setiap operator ? mempertahankan error sumbernya di dalam rantai sehingga tidak ada informasi yang hilang selama perjalanan ke atas. Contoh ini juga ditandai tidak-dijalankan karena anyhow adalah crate eksternal.

Rencana serupa hanya dengan pustaka standar#

Seluruh gagasan inti kedua crate itu berakar pada pustaka standar dan dapat ditulis tanpa dependensi. Trait Error menyediakan metode source untuk rantai penyebab, trait Display menyediakan pesan yang mudah dibaca, dan Box<dyn Error> menjadi wadah umum yang menerima error bertipe apa pun melalui implementasi From bawaan. Program berikut membangun tipe error kecil dengan kedua trait itu, lalu memakai main yang mengembalikan Result sebagai titik laporan terakhir.

rust batas-std.rs
use std::error::Error;
use std::fmt;
use std::num::ParseIntError;

#[derive(Debug)]
enum ErrorBatas {
    BukanAngka {
        masukan: String,
        sumber: ParseIntError,
    },
}

impl fmt::Display for ErrorBatas {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ErrorBatas::BukanAngka { masukan, .. } => {
                write!(f, "nilai batas {} bukan angka", masukan)
            }
        }
    }
}

impl Error for ErrorBatas {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            ErrorBatas::BukanAngka { sumber, .. } => Some(sumber),
        }
    }
}

fn baca_batas(teks: &str) -> Result<u32, ErrorBatas> {
    let nilai: u32 = teks
        .trim()
        .parse()
        .map_err(|sumber| ErrorBatas::BukanAngka {
            masukan: teks.to_string(),
            sumber,
        })?;
    Ok(nilai)
}

fn main() -> Result<(), Box<dyn Error>> {
    println!("batas utama: {}", baca_batas("80")?);
    println!("batas kedua: {}", baca_batas("delapan")?);
    Ok(())
}
text Keluaran
batas utama: 80
Error: BukanAngka { masukan: "delapan", sumber: ParseIntError { kind: InvalidDigit } }

Tipe ErrorBatas menampilkan pesan nilai batas delapan bukan angka melalui Display dan menautkan ParseIntError sebagai source sehingga rantai penyebab tetap dapat ditelusuri. Fungsi baca_batas membungkus kegagalan parse dengan map_err agar teks masukannya ikut terbawa ke dalam variant. Pada main, operator ? mengubah ErrorBatas menjadi Box<dyn Error> secara otomatis. Ketika nilai Err mencapai main, runtime Rust mencetak baris Error: diikuti representasi Debug error tersebut, lalu menghentikan program dengan kode keluar 1. Baris pertama keluaran membuktikan bahwa pernyataan sebelum kegagalan tetap berjalan normal.

Kode manual yang panjang pada program di atas itulah yang diringkas oleh thiserror dan anyhow. Crate yang pertama menuliskan Display, From, dan penanda source secara otomatis, sedangkan crate yang kedua menyediakan wadah serba-bisa beserta metode konteksnya. Untuk binary yang hanya memiliki satu atau dua jenis kegagalan, bentuk pustaka standar sering sudah memadai, dan dependensi eksternal baru terasa berguna ketika rantai panggilan panjang atau ketika jenis error harus dibedakan oleh banyak pemanggil.

Kesalahan umum#

Kesalahan pertama adalah memakai anyhow pada pustaka. Tipe anyhow::Error tidak dapat dicocokkan dengan pola oleh pemanggil sehingga API kehilangan informasi jenis kegagalan, dan seluruh pengguna pustaka dipaksa ikut bergantung pada crate tersebut. Simpan anyhow untuk binary dan untuk pengujian, serta sediakan tipe error terstruktur pada API publik pustaka.

Kesalahan kedua adalah menjadikan String sebagai tipe error. String mudah ditulis tetapi tidak mengimplementasikan std::error::Error, tidak memiliki rantai source, dan memaksa pemanggil membedakan penyebab dengan membandingkan potongan teks. Bungkus teks itu sebagai field pada enum error yang mengimplementasikan Display dan Error, sehingga struktur tetap terjaga tanpa kehilangan keringanan penulisan.

Kesalahan ketiga adalah mencetak error dari dalam pustaka dengan println! atau eprintln!. Pustaka tidak mengetahui apakah pemanggil ingin mencatat, membungkam, atau menerjemahkan kegagalannya, dan pencetakan dari dalam merusak protokol keluaran program pemanggil. Kembalikan error, lalu biarkan titik paling luar yang memutuskan bentuk laporannya.

Ringkasan#

  • Pustaka mengembalikan tipe error terstruktur yang menjadi bagian dari API-nya, sedangkan aplikasi mengumpulkan konteks dan mencetak laporan sekali di titik paling luar.
  • thiserror menuliskan Display, From, dan source secara otomatis lewat derive, sehingga operator ? dan rantai penyebab bekerja tanpa boilerplate.
  • anyhow menyediakan satu wadah error serba-bisa dengan context yang membangun lapisan pesan pada setiap propagasi.
  • Pola dasarnya tersedia di pustaka standar, yaitu Display untuk pesan, source untuk rantai penyebab, Box<dyn Error> sebagai wadah umum, dan main yang mengembalikan Result sebagai titik laporan.

Referensi#