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:

rust
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:

text
total belanja: Rp45000
galat: invalid digit found in string

Mari kita bedah kode di atas:

  1. Baris 1 (async fn parse_price(...) -> Result<u32, std::num::ParseIntError>): future yang dihasilkan fungsi ini bertipe Future<Output = Result<u32, ParseIntError>> — Result menjadi isi dari Output, sesuai konsep Chapter async fn.
  2. Baris 8 (total += parse_price(item).await?;): dua mekanisme bekerja berlapis di satu baris. .await menunggu future parse selesai, lalu ? membongkar Result — kalau galat, compute_total langsung kembali dengan galat parse yang sama; kalau berhasil, angkanya menambah total. Operator tanya tidak peduli konteksnya async atau tidak.
  3. Baris 15-18 (match compute_total(...).await { ... }): .await mendahului match karena yang di-match adalah Result hasil future, bukan future-nya. Urutan .await → match ini adalah pola baku menunggu dan menangani hasil sekaligus.
  4. Output kedua (galat: invalid digit found in string): teks "delapan ribu" gagal di-parse, galatnya merambat dari parse_price → ? → pemanggil → cabang Err, tanpa satu cabang if pun 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:

rust
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:

text
batas kredit ganda: Rp30000000
galat: No such file or directory (os error 2)

Mari kita bedah kode di atas:

  1. Baris 8 (let contents = read_file(path).await?;): galat bertipe io::Error keluar dari read_file, lalu ? mengonversinya menjadi Box<dyn Error> secara otomatis lewat trait From — konversi yang sama persis dengan dunia sinkron.
  2. Baris 9 (let limit: u32 = contents.trim().parse()?;): baris ini bisa menghasilkan ParseIntError, jenis galat yang berbeda dari baris sebelumnya, namun ? mengonversinya ke Box<dyn Error> juga — satu tipe penampung menampung dua sumber galat dalam satu fungsi.
  3. Baris 16 (fs::write(&path, "15000000").await...): berkas uji ditulis lebih dulu agar pemanggilan pertama fetch_credit_limit berjalan sukses dan menghasilkan 30000000 (dari 15000000 dikali dua).
  4. Output kedua (galat: No such file or directory (os error 2)): path /tmp/w5d-tidak-ada.txt tidak ada, io::Error dari read_to_string merambat keluar lewat ? di baris 8 dan tercetak utuh — galat io::Error mengimplementasikan Display dengan format bawaan sistem operasi.
  5. 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:

rust
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:

text
Ok(230)
Err("gudang makassar tidak terdaftar")

Mari kita bedah kode di atas:

  1. Baris 10-11 (task_a: tokio::task::JoinHandle<Result<u32, String>>): tipe parameter dinyatakan eksplisit agar terbaca gamblang — handle atas task yang kelak menghasilkan Result<u32, String> (lapisan dalam), di atas Result<_, JoinError> milik handle itu sendiri (lapisan luar).
  2. Baris 13-15 (task_a.await.unwrap_or_else(|err| ...)): await menghasilkan Result<Result<u32, String>, JoinError>. unwrap_or_else dari Section Penanganan Error mengambil lapisan dalam jika sukses; jika task gagal (misalnya panic!), closure mengubah JoinError menjadi Err(String) bertipe sama dengan galat aplikasi — dua lapisan kini rata menjadi satu Result<u32, String>. Metode JoinError juga menyediakan is_panic() untuk memeriksa penyebabnya bila perlu pembedaan.
  3. Baris 19 (Ok(result_a? + result_b?)): kedua hasil diratakan, ? mengambil angkanya; jika salah satu task mengembalikan Err, seluruh combine_stock gagal dengan galat task itu dan penjumlahan tidak pernah terjadi.
  4. Baris 24-26 (pemanggilan pertama): gudang Jakarta (150) dan Surabaya (80) diambil lewat dua task spawn dari Chapter Spawn Task, lalu digabungkan — hasil Ok(230).
  5. Baris 29-32 (pemanggilan kedua): gudang Makassar tidak terdaftar, sehingga task kedua menghasilkan Err dan penggabungan gagal dengan Err("gudang makassar tidak terdaftar") — variabel task_a/task_b boleh dideklarasi ulang karena nilai lama sudah berpindah ke dalam combine_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.