Future yang dijanjikan tidak selalu datang: berkas bisa hilang, teks bisa gagal di-parse, server bisa tidak menjawab, task bahkan bisa panic! di tengah jalan. Kabar baiknya, seluruh perangkat error handling dari Section Penanganan Error — Result, operator ?, Box<dyn Error> — bekerja persis sama di dalam async fn; kabar yang perlu dipelajari hanyalah beberapa jenis galat baru yang lahir khusus dari dunia async: Elapsed dari timeout, JoinError dari task yang gagal, dan pola Result bersarang saat hasil task diambil dari JoinHandle. Chapter penutup section ini merangkai ketiganya, dari galat future tunggal sampai penggabungan galat dari banyak task sekaligus.
Result di Dalam async fn #
Titik awalnya menenangkan: async fn boleh mengembalikan Result seperti fungsi biasa, dan ? di dalam tubuhnya berperilaku sama — mengembalikan galat lebih awal ke pemanggil. Yang berubah hanya lapisan terluarnya: pemanggil menerima future yang menghasilkan Result, sehingga .await harus ditulis sebelum match bisa menyentuh hasilnya:
async fn parse_price(text: &str) -> Result<u32, std::num::ParseIntError> {
text.trim().parse::<u32>()
}
async fn compute_total(items: &[&str]) -> Result<u32, std::num::ParseIntError> {
let mut total = 0;
for item in items {
total += parse_price(item).await?;
}
Ok(total)
}
#[tokio::main]
async fn main() {
match compute_total(&["15000", "8000", "22000"]).await {
Ok(total) => println!("total belanja: Rp{}", total),
Err(err) => println!("galat: {}", err),
}
match compute_total(&["15000", "delapan ribu"]).await {
Ok(total) => println!("total belanja: Rp{}", total),
Err(err) => println!("galat: {}", err),
}
}Output yang Dihasilkan:
total belanja: Rp45000
galat: invalid digit found in stringMari kita bedah kode di atas:
- Baris 1 (
async fn parse_price(...) -> Result<u32, std::num::ParseIntError>): future yang dihasilkan fungsi ini bertipeFuture<Output = Result<u32, ParseIntError>>—Resultmenjadi isi dariOutput, sesuai konsep Chapter async fn. - Baris 8 (
total += parse_price(item).await?;): dua mekanisme bekerja berlapis di satu baris..awaitmenunggu future parse selesai, lalu?membongkarResult— kalau galat,compute_totallangsung kembali dengan galat parse yang sama; kalau berhasil, angkanya menambahtotal. Operator tanya tidak peduli konteksnya async atau tidak. - Baris 15-18 (
match compute_total(...).await { ... }):.awaitmendahuluimatchkarena yang di-matchadalahResulthasil future, bukan future-nya. Urutan.await→matchini adalah pola baku menunggu dan menangani hasil sekaligus. - Output kedua (
galat: invalid digit found in string): teks"delapan ribu"gagal di-parse, galatnya merambat dariparse_price→?→ pemanggil → cabangErr, tanpa satu cabangifpun yang kita tulis manual.
Menyatukan Jenis Galat dengan Box #
Masalah klasik penanganan error kembali muncul di async: read_file menghasilkan io::Error, parse menghasilkan ParseIntError, dan timeout menghasilkan Elapsed — tiga jenis yang tidak bisa ditampung satu Result dengan tipe galat tunggal. Solusi yang sudah kita kenal, Box<dyn Error>, berfungsi mulus di sini karena operator ? memanggil From untuk mengonversi galat galat apa pun ke tipe kotak itu:
use tokio::fs;
async fn read_file(path: &str) -> Result<String, std::io::Error> {
fs::read_to_string(path).await
}
async fn fetch_credit_limit(path: &str) -> Result<u32, Box<dyn std::error::Error>> {
let contents = read_file(path).await?;
let limit: u32 = contents.trim().parse()?;
Ok(limit * 2)
}
#[tokio::main]
async fn main() {
let path = std::env::temp_dir().join("w5d-kredit.txt");
fs::write(&path, "15000000").await.expect("gagal menulis file");
match fetch_credit_limit(path.to_str().unwrap()).await {
Ok(limit) => println!("batas kredit ganda: Rp{}", limit),
Err(err) => println!("galat: {}", err),
}
match fetch_credit_limit("/tmp/w5d-tidak-ada.txt").await {
Ok(limit) => println!("batas kredit ganda: Rp{}", limit),
Err(err) => println!("galat: {}", err),
}
}Output yang Dihasilkan:
batas kredit ganda: Rp30000000
galat: No such file or directory (os error 2)Mari kita bedah kode di atas:
- Baris 8 (
let contents = read_file(path).await?;): galat bertipeio::Errorkeluar dariread_file, lalu?mengonversinya menjadiBox<dyn Error>secara otomatis lewat traitFrom— konversi yang sama persis dengan dunia sinkron. - Baris 9 (
let limit: u32 = contents.trim().parse()?;): baris ini bisa menghasilkanParseIntError, jenis galat yang berbeda dari baris sebelumnya, namun?mengonversinya keBox<dyn Error>juga — satu tipe penampung menampung dua sumber galat dalam satu fungsi. - Baris 16 (
fs::write(&path, "15000000").await...): berkas uji ditulis lebih dulu agar pemanggilan pertamafetch_credit_limitberjalan sukses dan menghasilkan 30000000 (dari 15000000 dikali dua). - Output kedua (
galat: No such file or directory (os error 2)): path/tmp/w5d-tidak-ada.txttidak ada,io::Errordariread_to_stringmerambat keluar lewat?di baris 8 dan tercetak utuh — galatio::ErrormengimplementasikanDisplaydengan format bawaan sistem operasi. - Tipe kembalian
Result<u32, Box<dyn std::error::Error>>: pola praktis untuk aplikasi dan binary; jika menulis pustaka yang konsumennya perlu mencocokkan jenis galat, enum galat kustom dari Section Penanganan Error tetap pilihan yang lebih tepat.
Galat dari Tugas Terpisah #
Lapisan terakhir menuntut perhatian khusus karena hasilnya bersarang. JoinHandle::await menghasilkan Result<Result<T, E>, JoinError> — lapisan luar menampung kegagalan task itu sendiri (dibatalkan atau panic!), sedangkan lapisan dalam menampung Result yang dikembalikan tubuh task. Fungsi penggabung berikut meratakan kedua lapisan itu sebelum menjumlahkan hasil dua task:
async fn fetch_stock(warehouse: &str) -> Result<u32, String> {
match warehouse {
"jakarta" => Ok(150),
"surabaya" => Ok(80),
other => Err(format!("gudang {} tidak terdaftar", other)),
}
}
async fn combine_stock(
task_a: tokio::task::JoinHandle<Result<u32, String>>,
task_b: tokio::task::JoinHandle<Result<u32, String>>,
) -> Result<u32, String> {
let result_a = task_a
.await
.unwrap_or_else(|err| Err(format!("tugas gagal: {}", err)));
let result_b = task_b
.await
.unwrap_or_else(|err| Err(format!("tugas gagal: {}", err)));
Ok(result_a? + result_b?)
}
#[tokio::main]
async fn main() {
let task_a = tokio::spawn(fetch_stock("jakarta"));
let task_b = tokio::spawn(fetch_stock("surabaya"));
let results = combine_stock(task_a, task_b).await;
println!("{:?}", results);
let task_a = tokio::spawn(fetch_stock("jakarta"));
let task_b = tokio::spawn(fetch_stock("makassar"));
let results = combine_stock(task_a, task_b).await;
println!("{:?}", results);
}Output yang Dihasilkan:
Ok(230)
Err("gudang makassar tidak terdaftar")Mari kita bedah kode di atas:
- Baris 10-11 (
task_a: tokio::task::JoinHandle<Result<u32, String>>): tipe parameter dinyatakan eksplisit agar terbaca gamblang — handle atas task yang kelak menghasilkanResult<u32, String>(lapisan dalam), di atasResult<_, JoinError>milik handle itu sendiri (lapisan luar). - Baris 13-15 (
task_a.await.unwrap_or_else(|err| ...)):awaitmenghasilkanResult<Result<u32, String>, JoinError>.unwrap_or_elsedari Section Penanganan Error mengambil lapisan dalam jika sukses; jika task gagal (misalnyapanic!), closure mengubahJoinErrormenjadiErr(String)bertipe sama dengan galat aplikasi — dua lapisan kini rata menjadi satuResult<u32, String>. MetodeJoinErrorjuga menyediakanis_panic()untuk memeriksa penyebabnya bila perlu pembedaan. - Baris 19 (
Ok(result_a? + result_b?)): kedua hasil diratakan,?mengambil angkanya; jika salah satu task mengembalikanErr, seluruhcombine_stockgagal dengan galat task itu dan penjumlahan tidak pernah terjadi. - Baris 24-26 (pemanggilan pertama): gudang Jakarta (150) dan Surabaya (80) diambil lewat dua task
spawndari Chapter Spawn Task, lalu digabungkan — hasilOk(230). - Baris 29-32 (pemanggilan kedua): gudang Makassar tidak terdaftar, sehingga task kedua menghasilkan
Errdan penggabungan gagal denganErr("gudang makassar tidak terdaftar")— variabeltask_a/task_bboleh dideklarasi ulang karena nilai lama sudah berpindah ke dalamcombine_stock.
Ringkasan #
Error handling async adalah error handling lama di atas lapisan baru: async fn menghasilkan future ber-Result, ? merambatkan galat lebih awal dengan perilaku identik, dan match tinggal didahului .await. Box<dyn Error> menyatukan io::Error, ParseIntError, dan rekan-rekannya lewat konversi From — satu tipe penampung dari berkas hilang sampai parse gagal. Lapisan paling khas async ada di JoinHandle: hasil task datang bersarang sebagai Result<Result<T, E>, JoinError>, diratakan dengan unwrap_or_else sebelum digabungkan antar task. Dengan ini delapan Chapter Pemrograman Async tuntas: dari alasan meninggalkan thread per koneksi, async fn, .await, runtime tokio, spawn, I/O non-blocking, select!, hingga galat — bekal yang akan terpakai penuh saat menyusun aplikasi nyata pada Section Proyek, dan sementara itu Section Unsafe Rust menanti untuk membuka lapisan bawah Rust yang selama ini kita biarkan tertutup.