Saat Anda membangun aplikasi dengan Node.js, penanganan kesalahan terasa hampir terlalu mudah pada awalnya. Anda membungkus rute dalam try-catch, mengirim kode status 500, dan klien memutuskan apa yang harus dilakukan selanjutnya. Model tersebut berfungsi baik untuk HTTP, tetapi akan berantakan saat Anda beralih ke pekerjaan latar belakang (background jobs). Dalam sistem antrean (queue system), tidak ada klien yang menunggu. Yang ada hanyalah worker, payload, dan penghitung retry yang terus bertambah di suatu tempat di Redis, RabbitMQ, atau SQS. Jika Anda menangani kegagalan dengan cara yang sama seperti menangani permintaan web yang gagal, Anda tidak hanya akan menjatuhkan satu transaksi. Anda akan menghambat seluruh pipeline Anda, menghabiskan daya komputasi, atau membuat worker Anda crash berulang kali pada pesan beracun (poisoned message) yang sama.
Pola Pikir HTTP Berantakan dalam Pekerjaan Latar Belakang
Dalam siklus request-response, loop umpan balik bersifat instan. Seorang pengguna mengklik tombol, server melempar error, dan pengguna melihat layar kegagalan. Pembersihan (cleanup) sangat sederhana. Seorang queue worker hidup dalam isolasi. Ia mengambil sebuah job, mengerjakannya selama beberapa detik atau menit, lalu mengakui keberhasilan (acknowledges success). Jika terjadi kesalahan di tengah jalan, antrean tidak tahu mengapa. Ia hanya tahu bahwa konfirmasi (acknowledgment) tidak pernah sampai. Tergantung pada konfigurasi Anda, ia akan mencoba lagi, mungkin selamanya. Satu payload yang salah format dapat berpindah-pindah di antara worker ratusan kali, membuang-buang CPU dan bersembunyi di balik job sah yang sebenarnya perlu diproses.
Dua Jenis Kegagalan
Aturan pertama dari antrean yang tangguh (resilient queues) adalah berhenti memperlakukan setiap error dengan cara yang sama. Anda perlu memilah kegagalan ke dalam dua kelompok saat kegagalan itu terjadi.
Kegagalan yang dapat diulang (retryable failures) bersifat transien. Pikirkan tentang timeout jaringan, rate limits dari API pihak ketiga, atau koneksi database yang terputus karena pool sempat habis. Ini adalah gejala dari sistem yang sedang berjalan di bawah tekanan. Mereka mungkin berhasil pada percobaan berikutnya dua menit dari sekarang.
Kegagalan permanen adalah poison pills. Ini termasuk payload yang salah format, kesalahan validasi skema, atau field wajib yang hilang karena layanan upstream mengubah kontraknya. Mengulanginya adalah pemborosan belaka. Mereka akan gagal dengan cara yang identik pada percobaan keseratus.
Jika blok catch Anda tidak dapat membedakan kedua hal ini, antrean Anda berjalan tanpa arah.
Pola 1: Klasifikasikan Error pada Blok Catch
Blok catch worker Anda harus menjadi kode yang paling terencana di dalam file tersebut. Saat sebuah error muncul, segera periksa. Apakah kode error-nya ECONNRESET atau timeout? Masukkan ke antrean untuk retry. Apakah itu SyntaxError, penolakan validasi Joi, atau batasan foreign key yang hilang? Pindahkan langsung ke dead-letter queue, atau DLQ, dan jangan hitung itu sebagai bagian dari batas retry Anda.
Sebagian besar library queue Node.js, termasuk BullMQ dan Bee Queue, memungkinkan Anda menentukan strategi backoff kustom dan error hooks. Gunakanlah. Error permanen tidak boleh tidur dan mencoba lagi tiga kali secara default. Error tersebut harus dievakuasi dari antrean utama agar job Anda yang lain dapat mengalir. DLQ melestarikan payload yang tepat dan konteks error, yang memungkinkan Anda memutar ulang (replay) job tersebut nanti setelah Anda menambal bug atau memperbaiki skema.
Pola 2: Exponential Backoff dengan Jitter
Melakukan retry secara instan sangatlah agresif. Jika database downstream sudah kewalahan karena beban, memukulnya lagi setiap dua detik dari lima puluh worker akan menghabisinya. Anda perlu menjauh dan memberi ruang bagi sistem untuk pulih.
Gunakan exponential backoff. Pada kegagalan pertama, tunggu satu detik. Pada yang kedua, tunggu dua detik. Kemudian empat, lalu delapan, hingga batas yang masuk akal seperti lima menit. Namun, waktu saja tidak cukup. Jika setiap job yang gagal menggunakan interval yang persis sama, mereka semua akan bertabrakan saat masa backoff berakhir. Gelombang sinkronisasi tersebut, yang terkadang disebut thundering herd, dapat membebani layanan yang sedang dalam masa pemulihan.
Tambahkan jitter. Ambil delay yang telah Anda hitung dan buatlah sedikit acak dengan persentase tertentu, mungkin sepuluh hingga dua puluh persen. Empat detik menjadi 4,2 atau 4,7 detik. Keacakan sederhana ini menyebarkan lonjakan retry dan menjaga infrastruktur Anda agar tidak dihantam secara bergelombang.
Pola 3: Desain untuk Idempotensi
Di sinilah infrastruktur antrean bertemu dengan logika bisnis. Bayangkan sebuah job yang menagih pelanggan melalui penyedia pembayaran. Worker berhasil mengirimkan tagihan, tetapi koneksi terputus sebelum ia dapat mencatat keberhasilan tersebut ke database Anda atau mengakui (acknowledge) job tersebut. Antrean melihat adanya kegagalan. Ia mencoba lagi. Pelanggan ditagih dua kali.
In Node.js, prevent this by making every side effect idempotent. Generate an idempotency key from the job ID or a business-specific identifier. Before you create the charge or send the email or adjust inventory, check whether the work was already done. Pass that key all the way through to your database and to any third-party APIs that accept one. Structure your jobs so that running the same payload ten times produces the same outcome as running it once. This single habit eliminates an entire category of financial and data-integrity bugs.
Pattern 4: Treat Your Dead-Letter Queue Like a Dashboard
A DLQ is not a graveyard where bad jobs go to be forgotten. It is an operational tool, and it should be one of your most watched surfaces.
Set up alerts that fire when the DLQ depth increases. Even a single message in the DLQ often means your validation logic is broken, an upstream schema changed, or a downstream service is sending garbage you no longer recognize. These are exactly the signals you want to catch before customers start complaining. Build a dashboard that lets you inspect the raw payload, the stack trace, and the timestamp. Have a runbook ready: inspect the failure, patch the code, then replay the messages in the correct order. If your queue implementation supports it, alert on growth rate as well as absolute count, because one bad deploy can flood the DLQ within minutes.
Pattern 5: When in Doubt, Crash the Process
Node.js runs on a single event loop inside a V8 isolate. An unhandled promise rejection or an
