Kebanyakan tutorial Node.js menganggap pengendalian ralat sebagai perkara sampingan. Anda membungkus pengendali laluan (route handler) dalam blok try/catch, mencatat jejak timbunan (stack trace), dan mengembalikan ralat 500. Pemikiran sedemikian kekal kerana ada manusia sebenar yang sedang menunggu di hujung permintaan HTTP. Tugasan latar belakang (background jobs) adalah berbeza. Dalam sistem barisan (queue system), tiada pelanggan yang tidak sabar untuk dibalas, tiada penyegaran pelayar automatik. Yang ada hanyanya seorang pekerja (worker), satu payload, dan pembilang cubaan semula yang berdetik dalam diam. Apabila berlaku kesilapan, ia berlaku secara perlahan, kemudian berlaku secara serentak. Satu ralat yang salah diklasifikasikan boleh melumpuhkan keseluruhan saluran (pipeline) atau mengejutkan jurutera pada pukul tiga pagi.
Ketidakpadanan ini mudah sahaja. Kitaran permintaan-respons (request-response) gagal dengan cepat dan jelas. Kegagalan barisan pula bersifat senyap. Seorang pekerja mungkin memproses beratus-ratus tugasan sebelum sambungan pangkalan data terputus. Tanpa peraturan pengendalian yang jelas, pekerja tersebut akan mencuba semula dengan serta-merta, menekan pangkalan data yang sedia terbeban, dan akhirnya ranap. Oleh kerana tiada sesiapa yang memerhati pekerja tersebut secara langsung, tanda pertama masalah selalunya adalah penimbunan berantai atau cakera yang penuh dengan fail log. Anda memerlukan lebih daripada sekadar blok catch. Anda memerlukan strategi yang melayan kegagalan berbeza dengan cara yang berbeza dan melindungi sistem yang lain daripada satu tugasan yang bermasalah.
Dua Jenis Kegagalan
Mulakan dengan membahagikan setiap ralat kepada salah satu daripada dua kategori.
Ralat yang boleh dicuba semula (retryable errors) adalah bersifat sementara (transient). Contohnya, masa tamat rangkaian (network timeout) ke API pihak ketiga, respons had kadar (rate-limit) 429, atau replika pangkalan data sementara yang ketinggalan daripada pangkalan data utama. Ini adalah simptom tekanan, bukan pepijat (bugs). Sistem mungkin dapat memulihkan dirinya sendiri dalam masa tiga puluh saat. Tugasan yang boleh dicuba semula layak mendapat peluang kedua, tetapi hanya di bawah keadaan yang terkawal.
Ralat kekal (permanent errors) adalah kesilapan. JSON yang tidak sah dalam payload, ID pengguna yang hilang, atau fail wajib yang tidak wujud dalam storan. Ralat ini akan gagal pada cubaan ke-100 sama seperti ia gagal pada cubaan pertama. Mencuba semula ralat ini membazirkan kitaran CPU, membuang slot barisan, dan mewujudkan tekanan balik (back-pressure) toksik yang melambatkan tugasan yang sihat. Satu-satunya tempat yang berguna untuk kegagalan kekal adalah dalam log, amaran, atau barisan surat mati (dead-letter queue). Ia tidak sepatutnya berada dalam gelung cubaan semula (retry loop).
Bina Enjin Keputusan
Klasifikasikan dengan segera. Jangan serahkan keputusan ini kepada rangka kerja (framework) barisan. Sebaik sahaja anda menangkap ralat, tentukan nasibnya.
Dalam praktiknya, ini bermakna mencipta kelas ralat tersuai (custom error classes) atau fungsi pembungkus (wrapper functions) yang memeriksa kegagalan sebelum ia dialirkan ke atas (bubbling it up). Jika pemacu (driver) pangkalan data mencetuskan tetapan semula sambungan (connection reset), pengendali anda harus menandakannya sebagai boleh dicuba semula. Jika pengesah payload mencetuskan ketidakpadanan skema (schema mismatch), tandakannya sebagai kekal. Banyak pemproses tugasan secara lalai mencuba semula segalanya, yang merupakan pilihan paling mahal yang boleh anda buat. Tolak tugasan kekal dengan serta-merta. Sama ada buang tugasan tersebut atau alihkan ke barisan surat mati (dead-letter queue) supaya ia tidak merosakkan saluran utama. Tabiat tunggal ini dapat mencegah kesan salji (snowball effects) dengan lebih dipercayai berbanding sebarang perubahan infrastruktur.
Berundur, Tetapi Lebih Bijak
Apabila anda melakukan cubaan semula, jangan lakukan dengan serta-merta. Jika pangkalan data tergendala, lonjakan pekerja yang menekannya setiap saat akan kelihatan seperti serangan penafian perkhidmatan (denial-of-service attack) dari dalam. Gunakan exponential backoff. Tunggu satu minit, kemudian lima, kemudian lima belas. Berikan ruang kepada sistem huluan (upstream system) untuk pulih.
Namun, exponential backoff sahaja tidak mencukupi. Jika seribu tugasan gagal pada masa yang sama kerana perkhidmatan dimulakan semula, jadual cubaan semula mereka akan selari. Mereka akan menyerang perkhidmatan tersebut secara serentak apabila ia kembali dalam talian, yang berpotensi menjatuhkannya semula. Tambah jitter: sedikit anjakan rawak (random offset) pada setiap kelewatan. Menyebarkan cubaan semula merentasi beberapa saat dapat mengelakkan serangan serentak (synchronized stampedes). Matematiknya mudah, tetapi kestabilan yang diperoleh adalah sangat besar.
Simpan Bukti
Barisan surat mati (dead-letter queue) adalah jejak audit anda, bukan tong sampah. Apabila sesuatu tugasan telah menghabiskan cubaan semula terakhirnya, jangan sekadar memadamnya. Pindahkan keseluruhan payload, bersama dengan konteks ralat dan sejarah cubaan semula, ke DLQ.
Ini mengekalkan bukti. Manusia boleh memeriksa tugasan tersebut, membaiki pepijat, dan memainkannya semula secara manual jika perlu. Lebih penting lagi, pantau kedalaman DLQ anda. Lonjakan mendadak dalam tugasan yang dihantar ke DLQ selalunya merupakan amaran awal bagi penggunaan (deploy) yang buruk, perubahan skema yang salah, atau vendor luaran yang melanggar kontrak mereka. Anggap pertumbuhan DLQ sebagai penunjuk utama (leading indicator), bukan penunjuk susulan (trailing indicator). Jika DLQ anda semakin penuh, sesuatu di bahagian huluan telah berubah dan pasukan anda perlu mengetahuinya sebelum tunggakan (backlog) merebak.
Reka Bentuk untuk Cubaan Semula
Design every job as if it will run twice, because it might. A worker can fail halfway through processing, get rescheduled, and execute again. If your job charges a customer, sends an email, or increments an inventory count, a naive retry creates duplicates.
The fix is idempotency. Before you perform a side effect, check whether it already happened. Use a unique identifier from the job payload as an idempotency key. Store that key in a short-lived cache or a database table with a uniqueness constraint. If the key exists, skip the work and return success. This turns retries from a risk into a harmless no-op. It takes a few extra lines of code, but it saves you from explaining to finance why revenue doubled overnight.
Protect the Process
Unbounded promise rejections and stray exceptions can kill a Node.js process without warning. In a worker, that means dropped jobs and an orchestrator scrambling to restart the container.
Register global handlers for unhandledRejection and uncaughtException. Their job is not to rescue the application. It is to perform the minimum cleanup necessary, then exit. Let Docker, Kubernetes, or systemd restart the worker with a clean memory state. Limping along after a global handler fires invites memory leaks and corrupted state. A fast, clean death is safer than a slow zombie process that processes jobs incorrectly. Trust your orchestrator to bring you back; do not try to outsmart a corrupted runtime.
Respect the Signal
Workers get shut down during deployments, scaling events, and node rotations. If your process dies the instant it receives SIGTERM, you abort whatever job is currently in flight. That job may never finish, and its retry counter might not even have incremented yet.
Listen for SIGTERM and SIGINT. When a signal arrives, stop pulling new jobs from the queue. Finish the current job if you can. Set a hard timeout, perhaps thirty seconds, after which you exit regardless. This graceful shutdown respects the queue and avoids false failures. Your deployment pipeline should treat a worker that exits cleanly as healthy, while a crashed worker should trigger an alert.
The Real Takeaway
Reliable queue handling is not about catching every error. It is about making deliberate decisions for each failure mode. Retry the transient ones with patience. Bury the permanent ones quickly. Shield your workers from stampedes, protect your data with idempotency keys, and let dying processes exit cleanly. When every failure has a defined path, three in the morning becomes just another hour. Your pipeline keeps moving, and your team keeps sleeping.
