يبدو أنّ تصحيح أخطاء الاستثناءات في تطبيقات الويب أمر بسيط: يمكنك إيقاف التنفيذ مؤقتًا عند حدوث خطأ والتحقّق من المشكلة. ولكن الطبيعة غير المتزامنة لـ JavaScript تجعل هذا الإجراء معقّدًا بشكل مفاجئ. كيف يمكن لأدوات مطوّري البرامج في Chrome معرفة وقت ومكان التوقف المؤقت عند مرور الاستثناءات عبر الوعود والوظائف غير المتزامنة؟
يتناول هذا المنشور تحديات التوقّع بالالتقاط، وهي قدرة "أدوات المطوّرين" على توقّع ما إذا كان سيتمّ رصد استثناء لاحقًا في الرمز البرمجي. سنستكشف سبب صعوبة ذلك وكيفية تحسينات V8 الأخيرة (محرك JavaScript الذي يشغّل Chrome) لجعله أكثر دقة، ما يؤدي إلى تجربة تصحيح أخطاء أكثر سلاسة.
أهمية توقّع عدد اللقطات
في "أدوات مطوّري البرامج في Chrome"، يتوفّر لك خيار إيقاف تنفيذ الرمز مؤقتًا للاستثناءات غير التي تمّ رصدها فقط، مع تخطّي الاستثناءات التي تمّ رصدها.
في الخلفية، يتوقف مصحِّح الأخطاء على الفور عند حدوث استثناء للحفاظ على السياق. وهذا توقّع لأنّه في هذه اللحظة، من المستحيل معرفة ما إذا كان سيتمّ رصد الاستثناء أم لا لاحقًا في الرمز، خاصةً في السيناريوهات غير المتزامنة. ويعود هذا الالتباس إلى الصعوبة المتأصلة في توقّع سلوك البرنامج، تمامًا مثل مشكلة التوقف.
فكِّر في المثال التالي: أين يجب أن يتوقف مصحِّح الأخطاء مؤقتًا؟ (ابحث عن إجابة في القسم التالي).
async function inner() {
throw new Error(); // Should the debugger pause here?
}
async function outer() {
try {
const promise = inner();
// ...
await promise;
} catch (e) {
// ... or should the debugger pause here?
}
}
يمكن أن يؤدي إيقاف استثناءات التصحيح مؤقتًا في أداة تصحيح الأخطاء إلى إيقاف عملية التصحيح مؤقتًا، ما يؤدي إلى حدوث انقطاعات متكررة وانتقال إلى رمز برمجي غير مألوف. للحدّ من ذلك، يمكنك اختيار تصحيح أخطاء الاستثناءات غير التي تمّ رصدها فقط، والتي يُرجّح أن تشير إلى أخطاء فعلية. ويعتمد ذلك على دقة توقّعات الصيد.
تؤدي التوقعات غير الصحيحة إلى الإحباط:
- النتائج السالبة الخاطئة (التوقّع بأنّه "لم يتم رصده" عندما سيتم رصده) عمليات التوقف غير الضرورية في أداة تصحيح الأخطاء
- الحالات الموجبة الخاطئة (التوقّع بأنّه تم رصد المخالفة عندما لا يكون الأمر كذلك) عدم رصد الأخطاء الخطيرة، ما قد يضطرك إلى تصحيح أخطاء جميع الاستثناءات، بما في ذلك الاستثناءات المتوقّعة
هناك طريقة أخرى لتقليل انقطاعات تصحيح الأخطاء وهي استخدام قائمة التجاهل التي تمنع حدوث انقطاعات في حالات الاستثناءات ضمن رمز تابع لجهة خارجية محدّد. ومع ذلك، لا يزال من الضروري توقّع عدد الأسماك التي سيتم صيدها بدقة. إذا تم تجاوز استثناء ناتج من رمز تابع لجهة خارجية وأثر في رمزك الخاص، ستحتاج إلى أن تكون قادرًا على تصحيح أخطاءه.
آلية عمل الرموز غير المتزامنة
يمكن أن تؤدّي طلبات الوعد وasync
وawait
وأنماط التنفيذ غير المتزامنة الأخرى إلى سيناريوهات قد يتّخذ فيها استثناء أو رفض مسار تنفيذ يصعب تحديده في وقت طرح الاستثناء. ويعود السبب في ذلك إلى أنّه قد لا يتم انتظار الوعود أو إضافة عناصر معالجة الاستثناءات إليها إلا بعد حدوث الاستثناء. لنلقِ نظرة على المثال السابق:
async function inner() {
throw new Error();
}
async function outer() {
try {
const promise = inner();
// ...
await promise;
} catch (e) {
// ...
}
}
في هذا المثال، تستدعي outer()
أولاً inner()
التي تُعرِض استثناءً على الفور. من هذا، يمكن لأداة تصحيح الأخطاء استنتاج أنّ inner()
ستُرجع وعدًا مرفوضًا، ولكن لا يتوفّر حاليًا أي وعد في انتظار المعالجة أو معالجته. يمكن لأداة تصحيح الأخطاء توقّع أنّ outer()
سينتظره على الأرجح، وتوقّع أنّه سيفعل ذلك في العبارة try
الحالية، وبالتالي معالجته، ولكن لا يمكن لأداة تصحيح الأخطاء التأكّد من ذلك إلا بعد عرض الوعد المرفوض والوصول إلى عبارة await
في النهاية.
لا يمكن لأداة تصحيح الأخطاء تقديم أي ضمانات بأنّ توقّعات التقاط الأخطاء ستكون دقيقة، ولكنها تستخدِم مجموعة متنوعة من الأساليب الاستقرائية لأنماط الترميز الشائعة للتوقّع بشكل صحيح. لفهم هذه الأنماط، من المفيد معرفة آلية عمل الوعود.
في الإصدار 8، يتم تمثيل Promise
JavaScript ككائن يمكن أن يكون في إحدى الحالات الثلاث: مكتمل أو مرفوض أو في انتظار المراجعة. إذا كان الوعد في الحالة "مكتفى به" وطلبت طريقة .then()
، سيتم إنشاء وعد جديد في انتظار المراجعة وسيتم جدولة مهمة جديدة للتفاعل مع الوعد ستشغّل معالِج الوعد ثم تضبط الوعد على "مكتفى به" مع نتيجة المعالِج أو ضبطه على "مرفوض" إذا تسبّب المعالِج في حدوث استثناء. يحدث الشيء نفسه إذا تم استدعاء طريقة .catch()
على وعد مرفوض. على العكس من ذلك، سيؤدي استدعاء .then()
على وعد مرفوض أو .catch()
على وعد محقَّق إلى عرض وعد في الحالة نفسها ولن يؤدي إلى تشغيل معالِج الحدث.
يحتوي الوعد المعلّق على قائمة ردود فعل حيث يحتوي كل عنصر ردّ فعل على معالِج إنجاز أو معالِج رفض (أو كليهما) ووعد ردّ فعل. وبالتالي، سيؤدي استدعاء .then()
على وعد في انتظار المراجعة إلى إضافة تفاعل مع معالِج مكتمل بالإضافة إلى وعد جديد في انتظار المراجعة لوعد التفاعل الذي ستُرجعه .then()
. سيؤدي استدعاء .catch()
إلى إضافة تفاعل مشابه ولكن مع معالِج رفض. يؤدي استدعاء .then()
مع وسيطتَين إلى إنشاء تفاعل مع كلا المعالجَين، وسيؤدي استدعاء .finally()
أو انتظار الوعد إلى إضافة تفاعل مع معالجَين هما دالة مدمجة خاصة بتنفيذ هذه الميزات.
عندما يتم في النهاية الوفاء بالوعد المعلّق أو رفضه، سيتم جدولة مهام التفاعلات لجميع معالِجاته التي تم الوفاء بها أو جميع معالِجاته التي تم رفضها. سيتم بعد ذلك تعديل طلبات التفاعل المقابلة، ما قد يؤدي إلى بدء مهام التفاعل الخاصة بها.
أمثلة
راجِع الرمز البرمجي التالي:
return new Promise(() => {throw new Error();})
.then(() => console.log('Never happened'))
.catch(() => console.log('Caught'));
قد لا يكون واضحًا أنّ هذا الرمز يتضمن ثلاثة كائنات Promise
مختلفة. التعليمة البرمجية أعلاه مكافئة للرمز البرمجي التالي:
const promise1 = new Promise(() => {throw new Error();});
const promise2 = promise1.then(() => console.log('Never happened'));
const promise3 = promise2.catch(() => console.log('Caught'));
return promise3;
في هذا المثال، تحدث الخطوات التالية:
- يتمّ استدعاء باني
Promise
. - يتم إنشاء
Promise
جديد في انتظار المراجعة. - يتم تشغيل الدالة المجهولة.
- يتم طرح استثناء. في هذه المرحلة، على أداة تصحيح الأخطاء تحديد ما إذا كان يجب التوقف أم لا.
- يرصد عنصر الإنشاء للوعد هذا الاستثناء، ثم يغيّر حالة الوعد إلى
rejected
مع ضبط قيمته على الخطأ الذي تم طرحه. ويعرض هذا الوعد الذي يتم تخزينه فيpromise1
. - لا يحدّد
.then()
أيّ وظيفة تفاعل لأنّpromise1
في الحالةrejected
. بدلاً من ذلك، يتم عرض وعد جديد (promise2
) يكون أيضًا في الحالة المرفوضة مع الخطأ نفسه. .catch()
تحدّد موعدًا لمهمة التفاعل باستخدام معالِج التفاعل المقدَّم ووعد تفاعل جديد في انتظار المراجعة، والذي يتم إرجاعه على النحو التالي:promise3
. في هذه المرحلة، يعلم مصحِّح الأخطاء أنّه سيتم التعامل مع الخطأ.- عند تنفيذ مهمة التفاعل، يعود المعالِج بشكلٍ طبيعي ويتم تغيير حالة
promise3
إلىfulfilled
.
يتضمّن المثال التالي بنية مشابهة، ولكنّ التنفيذ مختلف تمامًا:
return Promise.resolve()
.then(() => {throw new Error();})
.then(() => console.log('Never happened'))
.catch(() => console.log('Caught'));
ويعادل ذلك ما يلي:
const promise1 = Promise.resolve();
const promise2 = promise1.then(() => {throw new Error();});
const promise3 = promise2.then(() => console.log('Never happened'));
const promise4 = promise3.catch(() => console.log('Caught'));
return promise4;
في هذا المثال، تحدث الخطوات التالية:
- يتم إنشاء
Promise
بالحالةfulfilled
ويتم تخزينه فيpromise1
. - يتم جدولة مهمة تفاعل وعد باستخدام الدالة المجهولة الأولى ويتم عرض وعد التفاعل
(pending)
على النحو التالي:promise2
. - تتم إضافة تفاعل إلى
promise2
باستخدام معالِج مكتمل ووعد التفاعل الذي يتم إرجاعه على النحو التالي:promise3
. - تتم إضافة تفاعل إلى
promise3
باستخدام معالِج مرفوض ووعد تفاعل آخر، ويتم إرجاعه على النحو التالي:promise4
. - يتم تنفيذ مهمة التفاعل المُجدوَلة في الخطوة 2.
- يُعرِض معالِج الأحداث استثناءً. في هذه المرحلة، على أداة تصحيح الأخطاء تحديد ما إذا كان يجب التوقف أم لا. في الوقت الحالي، المعالج هو رمز JavaScript الوحيد الذي يتم تشغيله.
- بما أنّ المهمة تنتهي باستثناء، يتم ضبط وعد التفاعل المرتبط (
promise2
) على الحالة المرفوضة مع ضبط قيمته على الخطأ الذي تم طرحه. - بما أنّ
promise2
كان لديه تفاعل واحد، ولم يكن لهذا التفاعل معالِج مرفوض، تم ضبط وعد التفاعل (promise3
) أيضًا علىrejected
مع الخطأ نفسه. - بما أنّ
promise3
كان لديه تفاعل واحد، وكان لهذا التفاعل معالِج مرفوض، تم جدولة مهمة تفاعل وعد باستخدام هذا المعالِج ووعد التفاعل (promise4
). - عند تنفيذ مهمة التفاعل هذه، يعود المعالِج بشكلٍ طبيعي ويتم تغيير حالة
promise4
إلى "مكتملة".
طرق توقّع الصيد
هناك مصدران محتملان للمعلومات المتعلّقة بتوقّع الصيد. أحدهما هو حزمة التنفيذ. وهذا مناسب للأخطاء المتزامنة: يمكن لأداة تصحيح الأخطاء التنقّل في تسلسل استدعاء الدوالّ بالطريقة نفسها التي سيتّبعها رمز إلغاء تسلسل استدعاء الدوالّ، وتتوقف الأداة إذا عثرت على إطار يتضمّن كتلة try...catch
. بالنسبة إلى الوعود أو الاستثناءات المرفوضة في منشئي الوعود أو في الدوال غير المتزامنة التي لم يتم تعليقها مطلقًا، يعتمد مصحِّح الأخطاء أيضًا على تسلسل الاستدعاءات، ولكن في هذه الحالة، لا يمكن الاعتماد على توقّعاته في جميع الحالات. ويعود السبب في ذلك إلى أنّه بدلاً من طرح استثناء على أقرب معالِج، سيعرض الرمز غير المتزامن استثناءً مرفوضًا، وعلى أداة تصحيح الأخطاء إجراء بعض الافتراضات حول ما سيفعله المُرسِل به.
أولاً، يفترض أداة تصحيح الأخطاء أنّ الدالة التي تتلقّى عملية غير مكتملة من المرجّح أن تعرِض هذه العملية غير المكتملة أو عملية غير مكتملة مشتقة حتى تحصل الدوال غير المتزامنة في أعلى الحزمة على فرصة انتظارها. ثانيًا، يفترض مصحِّح الأخطاء أنّه إذا تم عرض وعد كدالة غير متزامنة، سينتظره قريبًا بدون الدخول إلى كتلة try...catch
أو الخروج منها أولاً. لا يمكن ضمان صحة أيّ من هذين الافتراضَين، ولكنّهما كافيان لإجراء التوقّعات الصحيحة لأنماط الترميز الأكثر شيوعًا باستخدام الدوالّ غير المتزامنة. في الإصدار 125 من Chrome، أضفنا طريقة استكشافية أخرى: يتحقّق مصحّح الأخطاء ممّا إذا كان المُستخدَم على وشك استدعاء .catch()
على القيمة التي سيتم إرجاعها (أو .then()
مع مَعلمتَين، أو سلسلة من طلبات الاستدعاء إلى .then()
أو .finally()
متبوعة بـ .catch()
أو .then()
مع مَعلمتَين). في هذه الحالة، يفترض مصحّح الأخطاء أنّ هذه هي الطرق الواردة في الوعد الذي نتتبّعه أو طريقة ذات صلة به، وبالتالي سيتم رصد الرفض.
المصدر الثاني للمعلومات هو شجرة ردود الفعل على الوعود. يبدأ مصحِّح الأخطاء بوعد الجذر. في بعض الأحيان، يكون هذا وعدًا تم استدعاء أسلوبه reject()
للتو. في أغلب الأحيان، عندما يحدث استثناء أو رفض أثناء مهمة تفاعل الوعد، ولا يبدو أنّ هناك أي شيء في تسلسل استدعاء الدوالّ يمكنه رصده، يتتبّع مصحّح الأخطاء الوعد المرتبط بالتفاعل. يفحص أداة تصحيح الأخطاء جميع ردود الفعل على الوعد المعلّق ويتحقّق مما إذا كانت تحتوي على معالجات رفض. وإذا لم تفعل أي تفاعلات ذلك، يفحص التفاعل المُعلَن عنه ويتتبّعه بشكل متكرّر. إذا كانت جميع التفاعلات تؤدي في النهاية إلى معالج رفض، يعتبر مصحِّح الأخطاء أنّه تم رصد رفض الوعد. هناك بعض الحالات الخاصة التي يجب مراعاتها، على سبيل المثال، عدم احتساب معالِج الرفض المضمّن لطلب .finally()
.
توفّر شجرة ردود الفعل على الوعد مصدرًا موثوقًا للمعلومات في العادة إذا كانت المعلومات متوفّرة. في بعض الحالات، مثل طلب Promise.reject()
أو في Promise
أو في دالة غير متزامنة لم تنتظر أي شيء بعد، لن تكون هناك ردود فعل لتتبُّعها، ويجب أن يعتمد مصحِّح الأخطاء على تسلسل استدعاء الدوال البرمجية فقط. في الحالات الأخرى، تحتوي شجرة ردود الفعل على الوعد عادةً على المعالِجات اللازمة لاستنتاج توقّع الالتقاط، ولكن من الممكن دائمًا إضافة المزيد من المعالِجات لاحقًا التي ستغيّر الاستثناء من تم الالتقاط إلى لم يتم الالتقاط أو العكس. هناك أيضًا تعهدات مثل تلك التي أنشأها Promise.all/any/race
، حيث قد تؤثر التعهدات الأخرى في المجموعة في كيفية التعامل مع الرفض. بالنسبة إلى هذه الطرق، يفترض مصحّح الأخطاء أنّه سيتم إعادة توجيه رفض الوعد إذا كان الوعد لا يزال في انتظار المراجعة.
اطّلِع على المثالَين التاليَين:
على الرغم من أنّ هذين المثالَين للاستثناءات التي تمّ رصدها يبدوان متشابهَين، إلا أنّهما يتطلّبان أساليب استنتاجية مختلفة تمامًا لتوقع حالات الصيد. في المثال الأول، يتم إنشاء وعد تم حلّه، ثم يتم جدولة مهمة ردّ فعل لـ .then()
ستؤدي إلى طرح استثناء، ثم يتم استدعاء .catch()
لإرفاق معالِج رفض بوعد الردّ. عند تنفيذ مهمة التفاعل، سيتم طرح الاستثناء، وستتضمّن شجرة التفاعل مع الوعد معالِج الالتقاط، وبالتالي سيتم رصده على أنّه تمّت معالجته. في المثال الثاني، يتم رفض الوعد على الفور قبل تنفيذ الرمز البرمجي لإضافة معالِج للخطأ، لذا لا تتوفّر معالِجات للرفض في شجرة ردود الفعل للوعد. يجب أن يفحص مصحِّح الأخطاء تسلسل استدعاء الدوالّ، ولكن لا تتوفّر أيضًا أيّ وحدات try...catch
. لتوقّع ذلك بشكل صحيح، يبحث مصحِّح الأخطاء عن الطلب الذي يُجريه .catch()
قبل الموقع الحالي في الرمز البرمجي، ويفترض على هذا الأساس أنّه سيتم التعامل مع الرفض في نهاية المطاف.
ملخّص
نأمل أن يكون هذا الشرح قد سلّط الضوء على آلية عمل ميزة توقّع الصيد في "أدوات مطوّري البرامج في Chrome"، ونقاط قوتها، والقيود المفروضة عليها. إذا واجهت مشاكل في تصحيح الأخطاء بسبب التوقّعات غير الصحيحة، ننصحك باتّباع الخيارات التالية:
- غيِّر نمط الترميز إلى نمط أكثر وضوحًا يمكن توقّعه، مثل استخدام الدوال غير المتزامنة.
- اختَر التوقف عند جميع الاستثناءات إذا تعذّر على "أدوات مطوّري البرامج" التوقف في الوقت المناسب.
- استخدِم نقطة إيقاف "عدم الإيقاف المؤقت هنا مطلقًا" أو نقطة إيقاف مشروطة إذا كان مصحِّح الأخطاء يتوقف في مكان لا تريده.
الشكر والتقدير
نشكر بشدة صوفيا إميليانوفا وجيسيلين يين على مساعدتهما القيّمة في تعديل هذا المنشور.