Memproses payload berukuran besar pada batch API atau webhook di Rust sering kali melibatkan transformasi data menggunakan iterator adaptor seperti .map(). Masalah muncul ketika fungsi transformasi menghasilkan Result<T, E>. Menangani tipe kembalian ini secara naif sering berujung pada silent failure akibat unwrap_or_default(), atau memicu panic mendadak di lingkungan produksi akibat anti-pattern unwrap().

Artikel ini membahas strategi idiomatik untuk mengelola error handling batch API di Rust menggunakan fitur bawaan trait Iterator, membandingkan pendekatan fail-fast, partial success, hingga evaluasi efisiensi alokasi memori.

Masalah: Result di Dalam Iterator Closure

Saat memetakan elemen koleksi menggunakan .map() dengan closure yang mengembalikan Result, tipe kembalian iterator bertransformasi menjadi Iterator<Item = Result<T, E>>. Dua kesalahan umum yang sering ditemukan:

  1. Anti-pattern .unwrap() atau .expect(): Memaksa ekstraksi nilai langsung di dalam closure. Jika ada satu item input yang tidak valid, seluruh worker thread API panik dan permintaan HTTP terputus.
  2. Silent failure via .filter_map(Result::ok): Mengabaikan error secara diam-diam. Payload yang korup dibuang tanpa logging atau audit, menghasilkan data yang hilang dan menyulitkan proses debugging.
// Anti-pattern: Satu record gagal langsung menyebabkan panic seluruh proses batch
let items: Vec<ValidatedItem> = raw_items
    .into_iter()
    .map(|raw| validate(raw).unwrap())
    .collect();

Pola 1: Fail-Fast dengan collect::<Result<Vec<T>, E>>()

Jika satu validasi gagal dan seluruh transaksi batch harus dibatalkan (skenario atomik), implementasi standar FromIterator di Rust memungkinkan collect membalikkan struktur dari Iterator<Item = Result<T, E>> menjadi Result<Vec<T>, E>.

Metode ini langsung menghentikan iterasi (short-circuit) begitu menemukan kegagalan pertama. Item berikutnya tidak akan diproses, menghemat alokasi memori dan siklus CPU.

#[derive(Debug, PartialEq)]
pub struct WebhookEvent {
    pub id: u64,
    pub event_type: String,
}

#[derive(Debug, PartialEq)]
pub enum ValidationError {
    InvalidId,
    MissingType,
}

pub fn validate_batch_fail_fast(
    raw_events: Vec<(u64, String)>,
) -> Result<Vec<WebhookEvent>, ValidationError> {
    raw_events
        .into_iter()
        .map(|(id, event_type)| {
            if id == 0 {
                return Err(ValidationError::InvalidId);
            }
            if event_type.is_empty() {
                return Err(ValidationError::MissingType);
            }
            Ok(WebhookEvent { id, event_type })
        })
        .collect() // Mengubah Iterator<Item = Result<T, E>> -> Result<Vec<T>, E>
}

#[test]
fn test_fail_fast() {
    let input = vec![(1, "charge.created".into()), (0, "user.deleted".into())];
    let result = validate_batch_fail_fast(input);
    assert_eq!(result, Err(ValidationError::InvalidId));
}

Pola 2: Partial Success dengan partition

Dalam pipeline ingestion webhook atau batch API massal, strategi fail-fast kerap tidak diinginkan. Sistem biasanya diwajibkan memproses data yang valid dan mengembalikan daftar kegagalan ke client.

Gunakan partition untuk memisahkan hasil valid dan error dalam satu kali pass traversal.

pub fn validate_batch_partial(
    raw_events: Vec<(u64, String)>,
) -> (Vec<WebhookEvent>, Vec<ValidationError>) {
    let (successes, failures): (Vec<_>, Vec<_>) = raw_events
        .into_iter()
        .map(|(id, event_type)| {
            if id == 0 {
                return Err(ValidationError::InvalidId);
            }
            if event_type.is_empty() {
                return Err(ValidationError::MissingType);
            }
            Ok(WebhookEvent { id, event_type })
        })
        .partition(Result::is_ok);

    (
        successes.into_iter().map(Result::unwrap).collect(),
        failures.into_iter().map(Result::unwrap_err).collect(),
    )
}
Catatan: Pemanggilan Result::unwrap dan Result::unwrap_err di atas dijamin aman (panic-free) karena elemen sudah dipisahkan menggunakan predikat Result::is_ok pada tahap sebelumnya.

Pola 3: Validasi Berantai dengan try_fold

Ketika transformasi elemen batch bergantung pada state atau membutuhkan akumulasi bersyarat dengan batas toleransi error (misalnya membatalkan proses hanya jika error melebihi threshold tertentu), Iterator::try_fold menyediakan kontrol imperatif secara fungsional.

pub fn validate_with_budget(
    raw_events: Vec<(u64, String)>,
    max_errors: usize,
) -> Result<Vec<WebhookEvent>, String> {
    let initial = (Vec::with_capacity(raw_events.len()), 0usize);

    let (valid_items, _) = raw_events.into_iter().try_fold(
        initial,
        |(mut acc, mut err_count), (id, event_type)| {
            if id == 0 || event_type.is_empty() {
                err_count += 1;
                if err_count > max_errors {
                    return Err("Error threshold exceeded".to_string());
                }
                Ok((acc, err_count))
            } else {
                acc.push(WebhookEvent { id, event_type });
                Ok((acc, err_count))
            }
        },
    )?;

    Ok(valid_items)
}

Analisis Performa: Iterator vs Manual For-Loop

Rust mengompilasi iterator chains menjadi zero-cost abstractions. Terdapat perbedaan profil memori yang signifikan antara pola iterasi dan for-loop manual:

  • For-Loop Konvensional: Kerap menyebabkan multiple re-allocation jika vektor penampung diinisialisasi menggunakan Vec::new() tanpa ukuran awal yang pasti (reserve).
  • collect::<Result<Vec<T>, E>>(): Menggunakan size_hint() dari iterator sumber untuk mengalokasikan kapasitas buffer Vec sekaligus di awal. Jika kegagalan terjadi di tengah stream, alokasi memori segera diputus dan sisa siklus pemrosesan dihentikan seketika.
  • Overhead partition(): Pola ini melakukan alokasi ke dua Vec yang terpisah. Jika jumlah elemen masukan besar, disarankan menggunakan loop manual dengan with_capacity untuk menghindari re-allokasi berulang pada vektor error.

Pemetaan Status Response HTTP

Gunakan status code HTTP yang akurat berdasarkan strategi penanganan yang dipilih:

  • Fail-Fast (Semua atau Tidak Sama Sekali): Kembalikan 400 Bad Request atau 422 Unprocessable Entity disertai detail satu item spesifik yang memicu error.
  • Partial Success: Kembalikan 207 Multi-Status (RFC 4918) dengan body JSON yang memuat array data yang berhasil diproses beserta array rincian error per record index.
  • Budget Exceeded: Kembalikan 422 Unprocessable Entity disertai keterangan bahwa batas kegagalan validasi payload terlampaui.