try/catch في جافا سكريبت: شبكة أمان لا حزام أمان
عندما يرمي سطر من أكواد جافا سكريبت خطأً، يتوقّف التنفيذ فورًا وينتشر الخطأ صعودًا عبر سلسلة الاستدعاءات (call stack). وإن لم يلتقطه أي كود، فالبرنامج ينهار (في Node)، أو يطبع جدارًا أحمر من الرسائل في الكونسول (في المتصفّح). وهنا يأتي دور try/catch لمعالجة الأخطاء في جافا سكريبت: فهو وسيلتك لاعتراض الخطأ والقول "أعلم أنّ هذا قد يفشل، وإليك ما أريد فعله بدلًا من الانهيار".
الشكل الأساسي:
JSON.parse يُطلق خطأً من نوع SyntaxError، فينتقل التنفيذ فوراً إلى كتلة catch مع ربط الخطأ بالمتغير err. أما الأمر console.log الثالث فيظل يعمل بشكل طبيعي، لأن الانهيار تمّ احتواؤه.
وإذا نجحت كتلة try دون إطلاق أي خطأ، فسيتم تجاوز كتلة catch بالكامل. وجودها مخصّص فقط لمسار الفشل.
كائن الخطأ في جافا سكريبت
أيّ شيء يُرمى عبر throw يُربط باسم المعامل داخل catch (...). وفي الغالب يكون نسخة من Error تحتوي على ثلاثة حقول مفيدة:
name يخبرك بنوع الخطأ الفرعي (TypeError أو RangeError أو SyntaxError وغيرها — سنتناولها بالتفصيل في الدرس التالي). أما message فهو الوصف المقروء للخطأ، وstack هو التتبع الكامل الذي يعتبر كنزاً حقيقياً أثناء تصحيح الأخطاء.
انتبه إلى نقطة مهمة: جافا سكريبت تسمح لك بـ throw لأي قيمة، وليس فقط كائنات Error. في الأكواد القديمة قد ترى أحياناً throw "something broke". لكن عندما تكتب throw الخاص بك، احرص دائماً على رمي كائن Error حتى يحصل المستدعي على تتبع كامل للمكدس:
كتلة finally تعمل في كل الأحوال
كتلة finally هي كتلة ثالثة اختيارية تُنفَّذ سواء وقع خطأ أم لا، وسواء التقطته كتلة catch أم لا. فائدتها الأساسية في عمليات التنظيف — كإغلاق الملفات، أو تحرير الأقفال، أو إخفاء مؤشرات التحميل:
يتم إخفاء مؤشر التحميل سواء نجحت العملية أو فشلت. لولا finally، لكان عليك تكرار هذا السطر في كلا الفرعين — ومن المؤكد أنك ستنساه في أحدهما.
الأجمل من ذلك أن finally ينفّذ حتى لو احتوى بلوك try أو catch على return. إذ ترجع الدالة القيمة بعد انتهاء تنفيذ finally. قد يبدو هذا السلوك مفاجئًا أحيانًا، لكنه في الغالب ما تحتاجه تمامًا.
كتلة catch ليست إلزامية دائمًا
وجود catch اختياري. فالجمع بين try/finally وحدهما جائز ومفيد عندما تريد ضمان تنفيذ عملية تنظيف دون أي نية للتعامل مع الخطأ — أي أنك تريد للخطأ أن يستمر في الانتشار:
الكتلة try/finally الداخلية تحرّر القفل حتى لو رمت fn() خطأً، لكنها لا تبتلع الخطأ — المستدعي يظل يراه. ابتلاع الأخطاء بصمت ("فشلت العملية ولم أخبر أحداً") من أسوأ كوابيس التصحيح التي ستواجهها.
إعادة رمي الأخطاء: عالج ما يهمك ومرّر الباقي
ليس مطلوباً من كتلة catch أن تعالج كل شيء. يمكنك فحص الخطأ، ومعالجة ما يناسبك، وإعادة رمي البقية للأعلى:
نمط instanceof هو القاعدة هنا: تعرّف على الأخطاء التي تعرف كيف تتعافى منها، واترك أي شيء آخر يصعد عبر المكدس بشكل طبيعي. ابتلاع كل الأخطاء داخل كتلة catch فارغة رائحته مريبة في الكود — فأنت تفقد كل إشارة تدلّك على وجود خلل غير متوقع.
استخدام try/catch مع async/await
داخل أي دالة async، الوعود (Promises) التي تنتظرها بـ await وتنتهي بالرفض تتحوّل إلى أخطاء مرميّة — وtry/catch يتعامل معها تمامًا كما يتعامل مع الأخطاء المتزامنة:
نقطة دقيقة يجب الانتباه لها: لا بدّ من استخدام await مع الـ Promise داخل كتلة try. فلو أعدت الـ Promise بدون await، سيحدث الرفض (rejection) بعد أن تكون الدالة قد انتهت فعلاً، ولن تلتقطه كتلة catch أبداً:
async function bad() {
try {
return fetch("/broken"); // بدون await — المستدعي يرى الرفض
} catch (err) {
// لا يتم تنفيذه أبداً
}
}
قاعدة عامة: داخل الدوال الـ async، استخدم await مع العملية التي تريد أن يغطيها try/catch.
try/catch متداخل
يمكنك تداخل كتل try/catch عندما يفشل الكود الداخلي والخارجي لأسباب مختلفة تحتاج إلى التعامل معها بطرق مختلفة:
الـ catch الداخلي يتعامل مع حالة "شكل البيانات غير صحيح" عن طريق إرجاع قيمة افتراضية آمنة، بينما الـ catch الخارجي يتعامل مع حالة "المدخل ليس JSON أصلاً" فيغلّف الخطأ ويعيد رميه. التداخل مقبول طالما أن كل طبقة لها استراتيجية تعافٍ مختلفة — أما إذا كانت الكتلتان ستفعلان الشيء نفسه، فالأفضل تبسيطها إلى مستوى واحد.
متى لا تستخدم try/catch
try/catch أداة مخصصة للأخطاء المتوقعة والقابلة للتعافي، وليس وسيلة لإخفاء العلل البرمجية.
- لا تغلّف جسم الدالة بالكامل "احتياطاً". إن لم يكن لديك خطة حقيقية للتعامل مع الخطأ، اترْكه يصعد لأعلى — فالخطأ غير الملتقَط ومعه stack trace أنفع بكثير من خطأ مكتوم.
- لا تستخدمه لأغراض التحكم في تدفق الكود. كتل
tryلها تكلفة فعلية على الأداء، وتجعل الكود أقل وضوحاً مقارنةً بفحصifبسيط.if (user)أفضل بكثير منtry { user.name } catch {}. - لا تلتقط الخطأ ثم تسجّله وتتجاهله. على الأقل أعِد رميه أو أرجع قيمة دلالية (sentinel) يستطيع المستدعي اكتشافها.
الاختبار الذهني المفيد هنا: "ماذا سيفعل مستخدم هذا الكود إذا فشلت هذه العملية؟" إن لم يكن لديك إجابة، فأنت لست مستعداً بعد لالتقاط الخطأ بـ catch.
مرجع سريع
try { ... } catch (err) { ... }— لاعتراض الأخطاء المرميّة.finally { ... }— ينفَّذ دائماً، استخدمه لعمليات التنظيف.throw new Error("...")— ارمِ دائماً كائنات مشتقّة منErrorحتى يعمل الـ stack trace بشكل صحيح.throw err;داخلcatch— لإعادة رمي الخطأ عندما لا تستطيع التعامل معه.awaitداخلtry— ضروري حتى يرىtry/catchحالات رفض الـ Promise.
التالي: أنواع الأخطاء
TypeError وRangeError وSyntaxError — جافا سكريبت تمتلك عائلة من كلاسات الأخطاء المدمجة، ومعرفة معنى كل واحد منها تجعل التقاط الأخطاء والإبلاغ عنها أكثر دقة بكثير. هذا موضوع الدرس القادم.
الأسئلة الشائعة
كيف يعمل try/catch في جافا سكريبت؟
تضع الكود المحتمل فشله داخل try { ... }. إذا حدث أي خطأ بداخله، ينتقل التنفيذ مباشرةً إلى كتلة catch (err) { ... }، وتُمرَّر قيمة الخطأ إلى المتغير err. أما إذا لم يحدث أي خطأ، فيتم تجاوز catch بالكامل. ويمكنك إضافة كتلة finally { ... } اختيارية تُنفَّذ في الحالتين، وهي مفيدة جدًا لعمليات التنظيف.
متى أستخدم try/catch في جافا سكريبت؟
استخدمه حول العمليات التي قد تفشل فعليًا أثناء التشغيل: مثل JSON.parse على مدخلات غير موثوقة، استجابات fetch، أو عمليات القراءة والكتابة على الشبكة والملفات. لا تُغلّف كل سطر في الكود؛ إذا لم يكن لديك خطة للتعافي من الخطأ، اترك الخطأ يصعد إلى الأعلى. فاستخدام try/catch بشكل فضفاض حول كود سليم يخفي الأخطاء بدلًا من معالجتها.
هل يلتقط try/catch الأخطاء غير المتزامنة؟
فقط إذا استخدمت await مع الـ Promise داخل كتلة try. استدعاء somePromise() مجردًا لن يتم التقاطه، وسيتحول الخطأ إلى unhandled rejection. مع async/await يعمل try/catch تمامًا كما يعمل مع الكود المتزامن. أما مع الـ Promises الخام، فاستخدم .catch() على السلسلة بدلًا من ذلك.
كيف أعيد رمي الخطأ (rethrow) في جافا سكريبت؟
داخل catch، اكتب throw err; ببساطة (أو ارمِ خطأً جديدًا يُغلّف الخطأ الأصلي). هذا مفيد عندما تريد معالجة بعض الأخطاء وترك الباقي يمر للأعلى: افحص نوع الخطأ أو رسالته، عالج ما تستطيع معالجته، وأعِد رمي البقية حتى تصل إلى المستدعين في الأعلى.