مکانیزم کامل مدیریت خطا: throw، try، catch و finally

در درس قبل با انواع مختلف خطا در جاوااسکریپت آشنا شدید. در این درس، تمرکز روی خود مکانیزم مدیریت خطا است: چگونه یک خطا به‌صورت دستی تولید می‌شود، ساختار دقیق بلوک try/catch چگونه کار می‌کند، و نکاتی ظریف اما مهم درباره‌ی رفتار finally که در بسیاری از منابع آموزشی نادیده گرفته می‌شوند.

throw: تولید دستی یک خطا

کلیدواژه‌ی throw اجرای عادی برنامه را بلافاصله متوقف کرده و کنترل را به نزدیک‌ترین بلوک catch منتقل می‌کند. نکته‌ی مهمی که کمتر مورد توجه قرار می‌گیرد این است که throw به هیچ نوع داده‌ای محدود نیست؛ هرچند همیشه بهترین شیوه، پرتاب یک شیء Error است:

function validateAge(age) {
  if (typeof age !== "number") {
    throw new TypeError("سن باید عدد باشد");
  }
  if (age < 0) {
    throw new RangeError("سن نمی‌تواند منفی باشد");
  }
  return age;
}

اگرچه فنی‌ای مانند throw "خطا" نیز در جاوااسکریپت مجاز است، اما استفاده از شیء Error ترجیح داده می‌شود، چون ویژگی‌های مفیدی مثل stack و name را در اختیار قرار می‌دهد که یک رشته‌ی ساده فاقد آن‌هاست.

ساختار try/catch

بلوک try کدی را که ممکن است خطا تولید کند در بر می‌گیرد؛ اگر خطایی رخ دهد، اجرای بلوک try بلافاصله متوقف شده و کنترل به بلوک catch منتقل می‌شود:

جریان اجرای try/catch
خروجی کنسول اینجا نمایش داده می‌شود...

همان‌طور که در خروجی مشخص است، به محض وقوع خطا در خط validateAge("بیست")، خط بعدی داخل همان بلوک try اصلاً اجرا نمی‌شود؛ اما برنامه پس از پایان بلوک catch، به‌طور عادی ادامه پیدا می‌کند.

دسترسی به شیء خطا در catch

پارامتر بلوک catch (در مثال بالا error) همان شیئی است که با throw پرتاب شده و تمام ویژگی‌های آن، از جمله message، name و stack، در دسترس است. در نسخه‌های مدرن جاوااسکریپت، حتی می‌توان این پارامتر را کاملاً حذف کرد اگر نیازی به استفاده از آن نباشد:

try {
  riskyOperation();
} catch {
  // بدون پارامتر خطا؛ زمانی مفید است که فقط نیاز به مدیریت کلی خطا داریم
  console.log("عملیات با شکست مواجه شد");
}

finally: نکته‌ی ظریفی که بسیاری نادیده می‌گیرند

بلوک finally صرف‌نظر از این‌که try موفق شود یا catch اجرا شود، همیشه اجرا می‌شود. اما نکته‌ی مهم‌تر و کمتر شناخته‌شده این است که finally حتی اگر داخل try یا catch دستور return نوشته شده باشد، پیش از خروج نهایی از تابع اجرا می‌شود:

function demo() {
  try {
    return "نتیجه‌ی try";
  } finally {
    console.log("finally همچنان اجرا می‌شود، حتی با وجود return");
  }
}

console.log(demo());
// خروجی به ترتیب:
// finally همچنان اجرا می‌شود، حتی با وجود return
// نتیجه‌ی try

نکته‌ی حیاتی‌تر و خطرناک‌تر این است که اگر داخل بلوک finally نیز یک return نوشته شود، این مقدار جایگزین هر مقدار بازگشتی از try یا catch می‌شود، حتی اگر آن مقدار قبلی از طریق throw باشد؛ این رفتار می‌تواند خطاها را به‌طور خاموش پنهان کند و به همین دلیل نوشتن return داخل finally یک ضدالگو (anti-pattern) در نظر گرفته می‌شود.

جدول رفتار سه بلوک

بلوک زمان اجرا
try همیشه ابتدا اجرا می‌شود
catch فقط اگر داخل try خطایی رخ دهد
finally همیشه اجرا می‌شود، حتی با وجود return یا throw در try/catch

نکات کلیدی

  • throw می‌تواند هر مقداری را پرتاب کند، اما استفاده از شیء Error به دلیل ویژگی‌هایی مثل stack ترجیح داده می‌شود.
  • پس از وقوع خطا در try، بقیه‌ی کد همان بلوک اجرا نمی‌شود و کنترل مستقیماً به catch منتقل می‌شود.
  • در نسخه‌های مدرن جاوااسکریپت، پارامتر catch در صورت عدم نیاز قابل حذف است.
  • finally همیشه اجرا می‌شود، حتی اگر try یا catch شامل return باشند؛ نوشتن return داخل خود finally یک ضدالگوی خطرناک است.

جمع‌بندی

مکانیزم throw/try/catch/finally ستون اصلی مدیریت خطا در جاوااسکریپت است. نکاتی مثل رفتار finally در برابر return و خطر پنهان‌شدن خطاها با return داخل خود finally، از جمله جزئیاتی هستند که شناخت آن‌ها، تفاوت میان کد قابل‌اعتماد و کدی با باگ‌های پنهان را رقم می‌زند. در ادامه‌ی این فصل، ساخت خطاهای سفارشی با ارث‌بری از Error بررسی خواهد شد.