Sebagian besar tutorial Node.js menganggap penanganan error sebagai hal yang sekunder. Anda membungkus route handler dalam blok try/catch, mencatat stack trace, dan mengembalikan status 500. Pola pikir ini bertahan karena ada orang sungguhan yang menunggu di ujung permintaan HTTP. Background jobs berbeda. Dalam sistem antrean, tidak ada klien tidak sabar untuk dibalas, tidak ada penyegaran browser otomatis. Yang ada hanyalah seorang worker, sebuah payload, dan penghitung retry yang terus berjalan dalam diam. Ketika terjadi kesalahan, kesalahan itu terjadi secara perlahan, lalu sekaligus. Satu error yang salah klasifikasi dapat menghentikan seluruh pipeline atau membangunkan engineer pada jam tiga pagi.
Ketidakterhubungannya sederhana. Siklus request-response gagal dengan cepat dan terlihat jelas. Kegagalan antrean bersifat sunyi. Seorang worker mungkin memproses ratusan job sebelum koneksi database terputus. Tanpa aturan penanganan yang jelas, worker akan mencoba lagi secara instan, menghantam database yang sudah kesulitan, dan akhirnya crash. Karena tidak ada yang mengawasi worker secara langsung, tanda pertama masalah sering kali berupa penumpukan (backup) yang beruntun atau disk yang penuh dengan file log. Anda butuh lebih dari sekadar blok catch. Anda butuh strategi yang memperlakukan kegagalan yang berbeda secara berbeda dan melindungi sisa sistem dari satu job yang buruk.
Dua Jenis Kegagalan
Mulailah dengan membagi setiap error ke dalam salah satu dari dua kategori.
Error yang dapat diulang (retryable) bersifat transien. Timeout jaringan ke API pihak ketiga, respons rate-limit 429, atau replika database sementara yang tertinggal dari primernya. Ini adalah gejala tekanan, bukan bug. Sistem mungkin bisa memulihkan dirinya sendiri dalam tiga puluh detik. Job yang retryable layak mendapatkan kesempatan lagi, tetapi hanya di bawah kondisi yang terkendali.
Error permanen adalah kesalahan. JSON tidak valid dalam payload, ID pengguna yang hilang, atau file wajib yang tidak ada di penyimpanan. Error ini akan gagal pada percobaan keseratus sama persis seperti saat mereka gagal pada percobaan pertama. Mengulanginya hanya akan membuang siklus CPU, menghabiskan slot antrean, dan menciptakan back-pressure beracun yang menunda job yang sehat. Satu-satunya tempat yang berguna untuk kegagalan permanen adalah log, peringatan (alert), atau dead-letter queue. Ia tidak seharusnya berada dalam loop retry.
Bangun Mesin Pengambil Keputusan
Klasifikasikan segera. Jangan serahkan keputusan ini kepada framework antrean. Saat Anda menangkap error, tentukan nasibnya.
Dalam praktiknya, ini berarti membuat custom error classes atau fungsi wrapper yang memeriksa kegagalan sebelum meneruskannya ke atas (bubbling up). Jika driver database melempar connection reset, handler Anda harus menandainya sebagai retryable. Jika validator payload melempar schema mismatch, tandai sebagai permanen. Banyak pemroses job secara default mengulang segalanya, yang merupakan pilihan paling mahal yang bisa Anda ambil. Tolak job permanen secara instan. Entah itu dibuang atau dialihkan ke dead-letter queue di mana mereka tidak dapat meracuni pipeline utama. Kebiasaan tunggal ini mencegah efek bola salju dengan lebih andal daripada perubahan infrastruktur apa pun.
Mundur Sejenak, Tapi Lebih Cerdas
Saat Anda melakukan retry, jangan pernah melakukannya secara instan. Jika database sedang down, lonjakan worker yang menghantamnya setiap detik akan terlihat seperti serangan denial-of-service dari dalam. Gunakan exponential backoff. Tunggu satu menit, lalu lima, lalu lima belas. Berikan ruang bagi sistem upstream untuk pulih.
Namun, exponential backoff saja tidak cukup. Jika seribu job gagal pada saat yang sama karena sebuah layanan dimulai ulang, jadwal retry mereka akan selaras. Mereka akan menghantam layanan tersebut secara bersamaan saat layanan itu kembali online, yang berpotensi menjatuhkannya lagi. Tambahkan jitter: offset acak kecil pada setiap penundaan. Menyebarkan retry di beberapa detik mencegah penyerbuan yang tersinkronisasi (synchronized stampedes). Matematikanya sederhana, tetapi stabilitas yang dihasilkannya sangat besar.
Simpan Buktinya
Dead-letter queue adalah jejak audit Anda, bukan tempat sampah. Ketika sebuah job menghabiskan jatah retry terakhirnya, jangan sekadar menghapusnya. Pindahkan seluruh payload, beserta konteks error dan riwayat retry, ke DLQ.
Ini menjaga bukti tetap ada. Manusia dapat memeriksa job tersebut, memperbaiki bug, dan memutarnya kembali (replay) secara manual jika diperlukan. Yang lebih penting, pantau kedalaman DLQ Anda. Lonjakan tiba-tiba pada job yang masuk ke dead-letter sering kali merupakan peringatan dini dari deploy yang buruk, perubahan skema yang salah, atau vendor eksternal yang melanggar kontrak mereka. Perlakukan pertumbuhan DLQ sebagai leading indicator, bukan trailing indicator. Jika DLQ Anda penuh, sesuatu di upstream telah berubah dan tim Anda perlu mengetahuinya sebelum backlog menyebar.
Rancang untuk Pengulangan (Retry)
Rancang setiap pekerjaan seolah-olah ia akan berjalan dua kali, karena hal itu mungkin saja terjadi. Seorang worker dapat gagal di tengah proses, dijadwalkan ulang, dan dieksekusi kembali. Jika pekerjaan Anda menagih pelanggan, mengirim email, atau menambah jumlah inventaris, upaya pengulangan (retry) yang naif akan menciptakan duplikasi.
Solusinya adalah idempotensi. Sebelum Anda melakukan efek samping (side effect), periksa apakah hal tersebut sudah terjadi. Gunakan pengenal unik dari payload pekerjaan sebagai kunci idempotensi (idempotency key). Simpan kunci tersebut dalam cache berumur pendek atau tabel database dengan batasan keunikan (uniqueness constraint). Jika kunci tersebut sudah ada, lewati pekerjaan tersebut dan kembalikan status sukses. Hal ini mengubah retry dari sebuah risiko menjadi operasi kosong (no-op) yang tidak berbahaya. Ini memang membutuhkan beberapa baris kode tambahan, tetapi akan menyelamatkan Anda dari kewajiban menjelaskan kepada bagian keuangan mengapa pendapatan berlipat ganda dalam semalam.
Lindungi Proses
Penolakan promise yang tidak terikat (unbounded promise rejections) dan pengecualian (exception) yang tidak tertangani dapat mematikan proses Node.js tanpa peringatan. Pada seorang worker, hal ini berarti pekerjaan yang terbuang dan orchestrator yang sibuk mencoba memulai ulang container.
Daftarkan handler global untuk unhandledRejection dan uncaughtException. Tugas mereka bukanlah untuk menyelamatkan aplikasi. Tugas mereka adalah melakukan pembersihan minimum yang diperlukan, lalu keluar (exit). Biarkan Docker, Kubernetes, atau systemd memulai ulang worker dengan status memori yang bersih. Berjalan tersendat-sendat setelah handler global aktif hanya akan mengundang kebocoran memori (memory leaks) dan status yang korup. Kematian yang cepat dan bersih lebih aman daripada proses zombie yang lambat dan memproses pekerjaan secara tidak benar. Percayalah pada orchestrator Anda untuk memulihkan Anda; jangan mencoba mengakali runtime yang sudah korup.
Hormati Sinyal
Worker akan dimatikan selama proses deployment, peristiwa penskalaan (scaling), dan rotasi node. Jika proses Anda mati seketika saat menerima SIGTERM, Anda akan membatalkan pekerjaan apa pun yang sedang berjalan. Pekerjaan tersebut mungkin tidak akan pernah selesai, dan penghitung retry-nya bahkan mungkin belum bertambah.
Dengarkan SIGTERM dan SIGINT. Saat sinyal tiba, berhentilah mengambil pekerjaan baru dari antrean (queue). Selesaikan pekerjaan yang sedang berjalan jika memungkinkan. Tetapkan batas waktu (hard timeout), mungkin tiga puluh detik, setelah itu Anda harus keluar apa pun yang terjadi. Penutupan yang anggun (graceful shutdown) ini menghormati antrean dan menghindari kegagalan palsu. Pipeline deployment Anda harus menganggap worker yang keluar dengan bersih sebagai sehat, sementara worker yang crash harus memicu peringatan.
Kesimpulan Utama
Penanganan antrean yang andal bukan tentang menangkap setiap kesalahan. Ini tentang membuat keputusan yang sengaja untuk setiap mode kegagalan. Lakukan retry pada kegagalan sementara (transient) dengan sabar. Tangani kegagalan permanen dengan cepat. Lindungi worker Anda dari lonjakan beban (stampedes), lindungi data Anda dengan kunci idempotensi, dan biarkan proses yang mati keluar dengan bersih. Ketika setiap kegagalan memiliki jalur yang ditentukan, jam tiga pagi hanyalah jam biasa lainnya. Pipeline Anda terus berjalan, dan tim Anda tetap bisa tidur nyenyak.
