Apabila anda membina aplikasi dengan Node.js, pengendalian ralat terasa hampir terlalu mudah pada mulanya. Anda membungkus satu laluan (route) dalam try-catch, menghantar kod status 500, dan klien akan memutuskan apa yang perlu dilakukan seterusnya. Model tersebut berfungsi dengan baik untuk HTTP, tetapi ia akan gagal sebaik sahaja anda beralih ke tugasan latar belakang (background jobs). Dalam sistem queue, tiada klien yang menunggu. Yang ada hanyalah pekerja (worker), payload, dan kaunter cubaan semula yang meningkat di suatu tempat dalam Redis, RabbitMQ, atau SQS. Jika anda mengendalikan kegagalan dengan cara yang sama seperti anda mengendalikan permintaan web yang gagal, anda bukan sahaja akan menggugurkan satu transaksi. Anda akan melambatkan keseluruhan saluran paip (pipeline) anda, membazirkan sumber pengkomputeran, atau menyebabkan pekerja anda terhenti (crash) berulang kali disebabkan mesej beracun (poisoned message) yang sama.
Pemikiran HTTP Gagal dalam Tugasan Latar Belakang
Dalam kitaran permintaan-respons (request-response), gelung maklum balas adalah serta-merta. Seorang pengguna mengklik butang, pelayan mencetuskan ralat, dan pengguna melihat skrin kegagalan. Pembersihan (cleanup) adalah mudah. Seorang pekerja queue beroperasi secara terasing. Ia mengambil satu tugasan, mengerjakannya selama beberapa saat atau minit, dan kemudian memberikan pengakuan (acknowledgment) kejayaan. Jika sesuatu berlaku di tengah-tengah proses, queue tidak tahu mengapa. Ia hanya tahu bahawa pengakuan tersebut tidak pernah sampai. Bergantung pada konfigurasi anda, ia akan mencuba semula, mungkin selama-lamanya. Satu payload yang tidak sah (malformed) boleh melantun di antara pekerja beratus-ratus kali, membazirkan CPU dan bersembunyi di sebalik tugasan sah yang sebenarnya memerlukan pemprosesan.
Dua Jenis Kegagalan
Peraturan pertama untuk queue yang berdaya tahan (resilient) adalah berhenti melayan setiap ralat dengan cara yang sama. Anda perlu mengasingkan kegagalan kepada dua kumpulan sebaik sahaja ia berlaku.
Kegagalan yang boleh dicuba semula (retryable failures) adalah bersifat sementara (transient). Contohnya seperti masa tamat rangkaian (network timeouts), had kadar (rate limits) daripada API pihak ketiga, atau sambungan pangkalan data yang terputus kerana pool telah habis seketika. Ini adalah simptom sistem yang sedang beroperasi di bawah tekanan. Ia mungkin berjaya pada cubaan seterusnya dua minit dari sekarang.
Kegagalan kekal (permanent failures) adalah 'poison pills'. Ini termasuk payload yang tidak sah, ralat pengesahan skema (schema validation errors), atau medan wajib yang hilang kerana perkhidmatan huluan (upstream service) telah mengubah kontraknya. Mencuba semula ralat ini adalah pembaziran semata-mata. Ia akan gagal dengan cara yang sama pada cubaan ke-seratus.
Jika blok catch anda tidak dapat membezakan antara kedua-dua jenis ini, sistem queue anda akan beroperasi tanpa hala tuju.
Corak 1: Klasifikasikan Ralat pada Blok Catch
Blok catch pekerja anda haruslah menjadi kod yang paling teliti dalam fail tersebut. Apabila ralat muncul, periksanya dengan segera. Adakah kod ralat itu ECONNRESET atau masa tamat (timeout)? Masukkannya ke dalam barisan untuk dicuba semula. Adakah ia SyntaxError, penolakan pengesahan Joi, atau kekangan kunci asing (foreign key constraint) yang hilang? Pindahkan ia terus ke dead-letter queue, atau DLQ, dan jangan kirakan ia terhadap had cubaan semula anda.
Kebanyakan perpustakaan queue Node.js, termasuk BullMQ dan Bee Queue, membenarkan anda menetapkan strategi backoff tersuai dan hook ralat. Gunakannya. Ralat kekal tidak sepatutnya tidur dan mencuba semula tiga kali secara lalai. Ia sepatutnya dikeluarkan dari queue utama supaya tugasan anda yang lain dapat mengalir lancar. DLQ mengekalkan payload yang tepat dan konteks ralat, yang membolehkan anda memainkan semula tugasan tersebut kemudian selepas anda membaiki pepijat atau membetulkan skema.
Corak 2: Exponential Backoff dengan Jitter
Mencuba semula secara serta-merta adalah tindakan yang agresif. Jika pangkalan data hiliran (downstream database) sudah pun bergelut di bawah beban kerja, menyerangnya semula setiap dua saat daripada lima puluh pekerja akan memusnahkannya. Anda perlu beralih ke belakang dan memberi ruang kepada sistem untuk pulih.
Gunakan exponential backoff. Pada kegagalan pertama, tunggu satu saat. Pada yang kedua, tunggu dua saat. Kemudian empat, kemudian lapan, sehingga mencapai had yang munasabah seperti lima minit. Namun, masa sahaja tidak mencukupi. Jika setiap tugasan yang gagal menggunakan selang masa yang sama tepat, semuanya akan bertembung apabila tempoh backoff tamat. Gelombang serentak itu, yang kadangkala dipanggil "thundering herd", boleh membebankan perkhidmatan yang sedang dalam proses pemulihan.
Tambah jitter. Ambil lengah (delay) yang telah dikira dan ubah suainya dengan peratusan rawak, mungkin sepuluh hingga dua puluh peratus. Empat saat menjadi 4.2 atau 4.7 saat. Rawak yang mudah ini akan menyebarkan lonjakan cubaan semula dan menghalang infrastruktur anda daripada terkena serangan secara bergelombang.
Corak 3: Reka Bentuk untuk Idempotensi
Di sinilah infrastruktur queue bertemu dengan logik perniagaan. Bayangkan satu tugasan yang mengenakan caj kepada pelanggan melalui penyedia pembayaran. Pekerja berjaya menghantar caj tersebut, tetapi sambungan terputus sebelum ia dapat merekodkan kejayaan ke dalam pangkalan data anda atau memberikan pengakuan kepada tugasan tersebut. Queue melihatnya sebagai kegagalan. Ia mencuba semula. Pelanggan pula dikenakan caj 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
