هنگام توسعه با Node.js، مدیریت خطا در ابتدا بسیار ساده به نظر میرسد. شما یک مسیر (route) را در یک بلوک try-catch قرار میدهید، کد وضعیت ۵00 را ارسال میکنید و کلاینت تصمیم میگیرد که در مرحله بعد چه کند. این مدل برای HTTP به خوبی کار میکند، اما به محض اینکه وارد حوزه کارهای پسزمینه (background jobs) شوید، از هم میپاشد. در یک سیستم صف، هیچ کلاینتی منتظر نیست. تنها یک ورکر (worker)، یک داده (payload) و یک شمارنده تلاش مجدد (retry counter) وجود دارد که در جایی مثل Redis، RabbitMQ یا SQS در حال بالا رفتن است. اگر با خطاها همانگونه برخورد کنید که با درخواستهای ناموفق وب برخورد میکنید، فقط یک تراکنش را از دست نخواهید داد؛ بلکه کل خط لوله (pipeline) خود را متوقف میکنید، منابع پردازشی را هدر میدهید یا ورکرهای خود را مدام بر سر یک پیام مسموم (poisoned message) از کار میاندازید.
ذهنیت HTTP در کارهای پسزمینه از کار میافتد
در چرخه درخواست-پاسخ، حلقه بازخورد فوری است. کاربر روی یک دکمه کلیک میکند، سرور خطایی صادر میکند و کاربر صفحه خطا را مشاهده میکند. پاکسازی (cleanup) ساده است. اما یک ورکرِ صف در انزوا زندگی میکند. او یک کار را برمیدارد، برای چند ثانیه یا دقیقه روی آن کار میکند و سپس موفقیتآمیز بودن آن را تأیید (acknowledge) میکند. اگر در میانه راه مشکلی پیش بیاید، صف هیچ ایدهای ندارد که چرا. او فقط میداند که تأییدیه هرگز نرسیده است. بسته به تنظیمات شما، صف ممکن است دوباره تلاش کند، و شاید این کار را تا ابد انجام دهد. یک دادهی ساختاریافتهی اشتباه (malformed payload) میتواند صدها بار بین ورکرها جابجا شود، باعث هدر رفتن CPU شود و پشت کارهای سالمی که واقعاً نیاز به پردازش دارند، پنهان شود.
دو نوع خطا
اولین قانون صفهای مقاوم این است که دیگر با همه خطاها به یک شکل برخورد نکنید. شما باید خطاها را در همان لحظه وقوع، به دو گروه تقسیم کنید.
خطاهای قابل بازگشت (Retryable failures) گذرا هستند. به زمانهای انتظار شبکه (network timeouts)، محدودیت نرخ (rate limits) از یک API شخص ثالث، یا قطع شدن اتصال پایگاه داده به دلیل اتمام ظرفیت موقت pool فکر کنید. اینها نشانههای یک سیستم زنده تحت فشار هستند. این خطاها ممکن است در تلاش بعدی، مثلاً دو دقیقه دیگر، با موفقیت انجام شوند.
خطاهای دائمی (Permanent failures) مانند قرصهای سمی هستند. اینها شامل دادههای ساختاریافتهی اشتباه، خطاهای اعتبارسنجی طرحواره (schema validation)، یا نبود یک فیلد اجباری به دلیل تغییر در قرارداد یک سرویس بالادستی است. تلاش مجدد برای اینها صرفاً اتلاف وقت است. آنها در صدمین تلاش نیز دقیقاً به همان شکل شکست خواهند خورد.
اگر بلوک catch شما نتواند تفاوت بین این دو را تشخیص دهد، صف شما در تاریکی حرکت میکند.
الگوی ۱: طبقهبندی خطاها در بلوک catch
بلوک catch در ورکر شما باید دقیقترین و حسابشدهترین بخش کد در فایل باشد. وقتی خطایی ظاهر میشود، بلافاصله آن را بررسی کنید. آیا کد خطا ECONNRESET است یا یک timeout؟ آن را برای تلاش مجدد در صف قرار دهید. آیا یک SyntaxError است، یا رد شدن در اعتبارسنجی Joi، یا نبود یک محدودیت کلید خارجی (foreign key constraint)؟ آن را مستقیماً به یک صف پیامهای مرده یا DLQ منتقل کنید و آن را جزو محدودیت تلاش مجدد خود حساب نکنید.
بسیاری از کتابخانههای صف Node.js، از جمله BullMQ و Bee Queue، به شما اجازه میدهند استراتژیهای بکآف (backoff) سفارشی و هوکهای خطا تعریف کنید. از آنها استفاده کنید. یک خطای دائمی هرگز نباید به طور پیشفرض سه بار بخوابد و دوباره تلاش کند. این خطا باید از صف اصلی تخلیه شود تا بقیه کارهای شما جریان یابند. DLQ دقیقاً همان داده و بافتار (context) خطا را حفظ میکند که به شما اجازه میدهد بعداً، پس از رفع باگ یا اصلاح طرحواره، آن کار را دوباره اجرا کنید.
الگوی ۲: بکآف نمایی همراه با جیتر (Exponential Backoff with Jitter)
تلاش مجدد فوری، رویکردی تهاجمی است. اگر یک پایگاه داده در پاییندست در حال حاضر زیر بار فشار در حال از کار افتادن است، ضربه زدن دوباره به آن هر دو ثانیه یکبار از سوی پنجاه ورکر، کار آن را تمام خواهد کرد. شما باید عقبنشینی کنید و به سیستم فرصت بازیابی بدهید.
از بکآف نمایی (exponential backoff) استفاده کنید. در اولین شکست، یک ثانیه صبر کنید. در دومین شکست، دو ثانیه. سپس چهار، سپس هشت، تا رسیدن به یک سقف منطقی مانند پنج دقیقه. اما زمانبندی به تنهایی کافی نیست. اگر هر کارِ شکستخورده از یک بازه زمانی دقیقاً یکسان استفاده کند، همگی هنگام پایان یافتن زمان بکآف با هم برخورد میکنند. این موج همزمان که گاهی اوقات "هجوم ناگهانی" (thundering herd) نامیده میشود، میتواند سرویسی را که در حال بازیابی است، از پا درآورد.
جیتر (Jitter) را اضافه کنید. تأخیر محاسبهشده خود را با یک درصد تصادفی، مثلاً ده تا بیست درصد، کمی تغییر دهید (fuzz کنید). چهار ثانیه تبدیل به ۴.۲ یا ۴.۷ ثانیه میشود. این تصادفیسازی ساده، اوج ناگهانی تلاشهای مجدد را پخش میکند و از ضربات موجی به زیرساخت شما جلوگیری میکند.
الگوی ۳: طراحی برای ایدمپوتنت بودن (Idempotency)
اینجاست که زیرساخت صف با منطق تجاری (business logic) تلاقی میکند. کاری را تصور کنید که هزینه مشتری را از طریق یک درگاه پرداخت کسر میکند. ورکر با موفقیت درخواست کسر وجه را ارسال میکند، اما قبل از اینکه بتواند موفقیتآمیز بودن را در پایگاه داده شما ثبت کند یا کار را تأیید (acknowledge) کند، اتصال قطع میشود. صف یک شکست را مشاهده میکند. دوباره تلاش میکند. و در نتیجه، از مشتری دو بار هزینه کسر میشود.
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
