Çoğu Node.js eğitimi, hata yönetimini sonradan akla gelen bir detay gibi ele alır. Bir route handler'ı try/catch bloğuna sarar, stack trace'i günlüğe kaydeder ve bir 500 hatası dönersiniz. Bu zihniyetin hayatta kalma sebebi, HTTP isteğinin diğer ucunda gerçek bir insanın bekliyor olmasıdır. Arka plan işleri (background jobs) ise farklıdır. Bir kuyruk sisteminde yanıt verecek sabırsız bir istemci veya otomatik bir tarayıcı yenilemesi yoktur. Sadece bir worker, bir payload ve sessizce artan bir yeniden deneme sayacı vardır. İşler ters gittiğinde, önce yavaş yavaş, sonra ise bir anda ters gider. Yanlış sınıflandırılmış tek bir hata, tüm bir veri hattını (pipeline) durdurabilir veya bir mühendisi gece saat üçte uykusundan uyandırabilir.

Aralarındaki kopukluk basittir. İstek-yanıt döngüleri hızlı ve gürültülü bir şekilde hata verir. Bir kuyruk hatası ise sessizdir. Bir worker, veritabanı bağlantısı kopmadan önce yüzlerce işi bitirmeye çalışabilir. Net hata yönetimi kuralları olmadan, worker anında yeniden deneme yapar, halihazırda zorlanan veritabanına yüklenir ve çöker. Worker'ı doğrudan kimse izlemediği için, sorunun ilk belirtisi genellikle zincirleme bir birikme veya disk dolusu log dosyasıdır. Sadece catch bloklarından daha fazlasına ihtiyacınız var. Farklı hataları farklı şekilde ele alan ve sistemin geri kalanını tek bir hatalı işten koruyan bir stratejiye ihtiyacınız var.

İki Tür Hata

Her hatayı iki kategoriden birine ayırarak işe başlayın.

Yeniden denenebilir (retryable) hatalar geçicidir. Üçüncü taraf bir API'ye yapılan ağ zaman aşımı (network timeout), bir 429 hız sınırı (rate-limit) yanıtı veya ana veritabanının gerisinde kalan geçici bir veritabanı replikası. Bunlar hata (bug) değil, baskı belirtileridir. Sistem otuz saniye içinde kendi kendini iyileştirebilir. Yeniden denenebilir işler, ancak kontrollü koşullar altında bir şans daha hak eder.

Kalıcı (permanent) hatalar ise hatalardır. Payload içindeki geçersiz bir JSON, eksik bir kullanıcı kimliği (user ID) veya depolama alanında bulunmayan zorunlu bir dosya. Bunlar, ilk denemedeki gibi yüzüncü denemede de başarısız olacaktır. Bunları yeniden denemek CPU döngülerini tüketir, kuyruk slotlarını boşa harcar ve sağlıklı işleri geciktiren toksik bir geri basınç (back-pressure) oluşturur. Kalıcı bir hata için tek yararlı yer bir log, bir uyarı veya bir dead-letter queue'dur. Yeniden deneme döngüsünün (retry loop) bir parçası olmamalıdır.

Bir Karar Mekanizması Oluşturun

Hemen sınıflandırın. Bu kararı kuyruk çerçevesine (queue framework) bırakmayın. Bir hatayı yakaladığınız anda kaderine karar verin.

Pratikte bu, hatayı yukarı fırlatmadan (bubbling up) önce inceleyen özel hata sınıfları (custom error classes) veya sarmalayıcı fonksiyonlar (wrapper functions) oluşturmak anlamına gelir. Eğer bir veritabanı sürücüsü bir bağlantı sıfırlama (connection reset) hatası fırlatırsa, handler'ınız bunu "yeniden denenebilir" olarak işaretlemelidir. Eğer bir payload doğrulayıcısı bir şema uyumsuzluğu (schema mismatch) fırlatırsa, bunu "kalıcı" olarak işaretleyin. Birçok iş işlemcisi varsayılan olarak her şeyi yeniden denemeye ayarlanmıştır ki bu yapabileceğiniz en maliyetli seçimdir. Kalıcı işleri anında reddedin. Ya onları bırakın ya da ana veri hattını zehirleyemeyecekleri bir dead-letter queue'ya yönlendirin. Bu tek alışkanlık, herhangi bir altyapı değişikliğinden daha güvenilir bir şekilde kartopu etkisini önler.

Geri Çekilin, Ama Daha Akıllıca

Yeniden deneme yapacağınız zaman bunu asla hemen yapmayın. Eğer bir veritabanı çökmüşse, her saniye ona yüklenen bir grup worker, içeriden yapılmış bir hizmet reddi (denial-of-service) saldırısı gibi görünecektir. Üstel geri çekilme (exponential backoff) kullanın. Önce bir dakika, sonra beş, sonra on beş dakika bekleyin. Üst sistemin (upstream system) toparlanması için ona alan tanıyın.

Ancak sadece üstel geri çekilme yeterli değildir. Eğer bir servis yeniden başlatıldığı için binlerce iş aynı anda başarısız olursa, yeniden deneme programları birbiriyle hizalanacaktır. Servis tekrar çevrimiçi olduğunda, ona aynı anda saldıracaklar ve potansiyel olarak tekrar çökmesine neden olacaklardır. Jitter ekleyin: her gecikmeye küçük, rastgele bir sapma. Yeniden denemeleri birkaç saniyeye yaymak, senkronize izdihamları (synchronized stampedes) önler. Matematik basittir ancak sağladığı kararlılık muazzamdır.

Kanıtları Koruyun

Bir dead-letter queue, bir çöp kutusu değil, bir denetim izidir (audit trail). Bir iş son yeniden deneme hakkını tükettiğinde, onu sadece silmeyin. Tüm payload'u, hata bağlamı (error context) ve yeniden deneme geçmişiyle birlikte bir DLQ'ya taşıyın.

Bu, kanıtları korur. Bir insan işi inceleyebilir, hatayı düzeltebilir ve gerekirse manuel olarak tekrar oynatabilir (replay). Daha da önemlisi, DLQ derinliğinizi izleyin. Dead-lettered işlerdeki ani bir artış, genellikle hatalı bir dağıtımın (deploy), yanlış giden bir şema değişikliğinin veya bir dış tedarikçinin sözleşmesini ihlal etmesinin en erken uyarısıdır. DLQ büyümesini bir sonuç göstergesi (trailing indicator) olarak değil, bir öncü gösterge (leading indicator) olarak değerlendirin. Eğer DLQ doluyorsa, üst tarafta bir şeyler değişmiştir ve birikmiş işler (backlog) yayılmadan ekibinizin bunu bilmesi gerekir.

Yeniden Deneme İçin Tasarlayın

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.