تُعدّ سلاسل الوعود ممتازة في التعامل مع الأخطاء، فمتى رُفض الوعد ينتقل سير التحكّم إلى أقرب دالة تتعامل مع حالة الرفض، وهذا عمليًا يسهّل الأمور كثيرًا.
فمثلًا نرى في الشيفرة أسفله أنّ العنوان المرّر إلى fetch
خطأ (ما من موقع بهذا العنوان) ويتعامل التابِع .catch
مع الخطأ:
fetch('https://no-such-server.blabla') // هذا الموقع من وحي الخيال العلمي .then(response => response.json()) .catch(err => alert(err)) // TypeError: failed to fetch (يمكن أن يتغيّر النصّ بتغيّر الخطأ)
وكما ترى فليس ضروريًا أن يكون التابِع .catch
مباشرةً في البداية، بل يمكن أن يظهر بعد تابِع .then
واحد أو أكثر حتّى.
أو قد يكون الموقع سليمًا ولكنّ الردّ ليس كائن JSON صالح. الطريقة الأسهل في هذه الحالة لالتقاط كلّ الأخطاء هي بإضافة تابِع .catch
إلى نهاية السلسلة:
fetch('/article/promise-chaining/user.json') .then(response => response.json()) .then(user => fetch(`https://api.github.com/users/${user.name}`)) .then(response => response.json()) .then(githubUser => new Promise((resolve, reject) => { let img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "promise-avatar-example"; document.body.append(img); setTimeout(() => { img.remove(); resolve(githubUser); }, 3000); })) .catch(error => alert(error.message)); // هنا
الطبيعي هو ألّا يعمل تابِع .catch
مطلقًا، ولكن لو رُفض أحد الوعود أعلاه (بسبب مشكلة في الشبكة أو كائن غير صالح أو أيّ شيء آخر)، فسيستلم التابِع الخطأ.
صياغة try..catch الضمنية
تُحيط بشيفرتي مُنفّذ الوعود ودوال مُعاملة الوعود عبارة try..catch
مخفية إن صحّ القول، فإن حدث استثناء يُلتقط ويتعامل معه المحرّك على أنّه حالة رفض. خُذ مثلًا هذه الشيفرة:
new Promise((resolve, reject) => { throw new Error("Whoops!"); // لاحِظ }).catch(alert); // Error: Whoops!
… وهل تعمل تمامًا مثل عمل هذه:
new Promise((resolve, reject) => { reject(new Error("Whoops!")); // لاحِظ }).catch(alert); // Error: Whoops!
تتلقّى عبارة try..catch
المخفي المُحيطة بالمُنفّذ الأخطاءَ تلقائيًا وتحوّلها إلى وعود مرفوضة.
ولا يحدث هذا في دالة المُنفّذ فحسب بل أيضًا في دوال المُعاملة. فلو رمينا شيئًا في دالة المُعاملة للتابِع throw
فيعني ذلك بأنّ الوعد رُفض وينتقل سير التحكّم إلى أقرب دالة تتعامل مع الأخطاء.
إليك مثالًا:
new Promise((resolve, reject) => { resolve("ok"); }).then((result) => { throw new Error("Whoops!"); // نرفض الوعد }).catch(alert); // Error: Whoops!
وهذا يحدث مع الأخطاء كافّة وليس فقط لتلك التي رُميت بعبارة throw
. حتّى أخطاء المبرمجين، انظر:
new Promise((resolve, reject) => { resolve("ok"); }).then((result) => { blabla(); // ما من دالة كهذه }).catch(alert); // ReferenceError: blabla is not defined
تابِع .catch
الأخير لا يستلم حالات الرفض الصريحة فحسب، بل تلك التي تحدث أحيانًا في دوال المُعاملة أعلاه أيضًا.
إعادة الرمي
كما رأينا فوجود تابِع .catch
نهاية السلسلة شبيه بعبارة try..catch
. يمكن أن نكتب ما نريد من دوال مُعاملة .then
وثمّ استعمال تابِع .catch
واحد في النهاية للتعامل مع أخطائها.
في عبارات try..catch
العاديّة نحلّل الخطأ ونُعيد رميه لو لم نستطع التعامل معه. ذات الأمر مع الوعود.
فإن رمينا شيئًا داخل .catch
، ينتقل سير التحكّم إلى أقرب دالة تتعامل مع الأخطاء. وإن تعاملنا مع الخطأ كما يجب فيتواصل إلى أقرب دالة .then
.
في المثال أسفله يتعامل التابِع .catch
مع الخطأ كما ينبغي:
// سلسلة التنفيذ: catch -> then new Promise((resolve, reject) => { throw new Error("Whoops!"); }).catch(function(error) { alert("The error is handled, continue normally"); }).then(() => alert("Next successful handler runs"));
لا تنتهي هنا كتلة .catch
البرمجية بأيّ أخطاء، بذلك تُستدعى دالة المُعاملة في تابِع .then
التالي.
نرى في المثال أسفله الحالة الثانية للتابِع .catch
. تلتقط دالة المُعاملة عند (*)
الخطأ ولكن لا تعرف التعامل معه (مثلًا لا تعرف إلّا أخطاء URIError
) فترميه ثانيةً:
// سلسلة التنفيذ: catch → catch → then new Promise((resolve, reject) => { throw new Error("Whoops!"); }).catch(function(error) { // (*) if (error instanceof URIError) { // handle it } else { alert("Can't handle such error"); *!* throw error; // رمي هذا الخطأ أو أي خطأ آخر سينقُلنا إلى catch التالية */!* } }).then(function() { /* لن يعمل هنا */ }).catch(error => { // (**) alert(`The unknown error has occurred: ${error}`); // لن يعيد أي شيء=>عملية التنفيذ حدثت بطريقة عادية });
ينتقل سير التنفيذ من تابِع .catch
الأول عند (*)
إلى التالي عند (**)
في السلسلة.
حالات رفض
ماذا يحدث لو لم نتعامل مع الخطأ؟ فنقل مثلًا نسينا إضافة تابِع .catch
في نهاية السلسلة هكذا:
new Promise(function() { noSuchFunction(); // خطأ (لا يوجد هذا التابع) }) .then(() => { // معالجات الوعد الناجح سواءً واحدة أو أكثر }); // بدون .catch في النهاية!
لو حدث خطأ يُرفض الوعد وعلى سير التنفيذ الانتقال إلى أقرب دالة تتعامل مع حالات الرفض. ولكن ما من دالة كهذه و”يعلق“ الخطأ إن صحّ التعبير إذ ما من شيفرة تتعامل معه.
عمليًا يتشابه هذا مع الأخطاء التي لم نتعامل معها في الشيفرات، أي أنّ شيئًا مريعًا قد حدث.
تتذكّر ما يحدث لو حدث خطأ عادي ولم تلتقطه عبارة try..catch
؟ ”يموت“ النص البرمجي ويترك رسالةً في الطرفية. ذات الأمر يحدث مع حالات رفض الوعود التي لم يجري التعامل معها.
يتعقّب محرّك جافاسكربت هذه الحالات ويولّد خطأً عموميًا في هذه الحالة. يمكنك أن تراه في الطرفية إن شغلت المثال أعلاه.
يمكننا في المتصفّحات التقاط هذه الأخطاء باستعمال الحدث unhandledrejection
:
*!* window.addEventListener('unhandledrejection', function(event) { // لدى كائن الحدث خاصيتين مميزتين: alert(event.promise); // [object Promise] - الوعد الذي يولد الخطأ alert(event.reason); // Error: Whoops! - كائن الخطأ غير المعالج }); */!* new Promise(function() { throw new Error("Whoops!"); }); // لا يوجد catch لمعالجة الخطأ
إنّما الحدث هو جزء من معيار HTML.
لو حدث خطأ ولم يكن هناك تابِع .catch
فيُشغّل معالج unhandledrejection
ويحصل على كائن الحدث مع معلومات حول الخطأ حتى نتمكن من فعل شيء ما.
مثل هذه الأخطاء عادةً ما تكون غير قابلة للاسترداد، لذلك أفضل طريقة للخروج هي إعلام المستخدم بالخطأ أو حتى إبلاغ الخادم بالخطأ.
ثمّة في البيئات الأخرى غير المتصفّحات (مثل Node.js) طرائقَ أخرى لتعقّب الأخطاء التي لم نتعامل معها.
خلاصة
-
يتعامل
.catch
مع الأخطاء في الوعود أيًا كانت: أكانت من استدعاءاتreject()
أو من رمي الأخطاء في دوال المُعاملة. -
يجب أن نضع
.catch
في الأماكن التي نريد أن نعالج الخطأ فيها ومعرفة كيفية التعامل معها. يجب على المعالج تحليل الأخطاء (الأصناف المخصصة تساعدنا بذلك) وإعادة رمي الأخطاء غير المعروفة (ربما تكون أخطاء برمجية). -
لا بأس بعدم استخدام
.catch
مطلقًا، إن لم يكُ هنالك طريقة للاسترداد من الخطأ. -
على أية حال، يجب أن يكون لدينا معالج الأحداث
unhandledrejection
(للمتصفحات وللبيئات الأخرى) وذلك لتتبع الأخطاء غير المُعالجة وإعلام المستخدم (وربما إعلام الخادم) عنها. حتى لا يموت تطبيقنا مطلقًا.
تمارين
خطأ في تابِع setTimeout
ما رأيك هل ستعمل .catch
؟ وضح إجابتك.
new Promise(function(resolve, reject) { setTimeout(() => { throw new Error("Whoops!"); }, 1000); }).catch(alert);
الجواب: لا لن تعمل:
new Promise(function(resolve, reject) { setTimeout(() => { throw new Error("Whoops!"); }, 1000); }).catch(alert);
كما ذكرنا في هذا الفصل لدينا "صياغة try..catch
ضمنية" حول تابع معيّن. لذلك تُعالج جميع الأخطاء المتزامنة.
ولكن هنا الخطأ لا يُنشأ عندما يعمل المُنفذّ وإنما في وقت لاحق. لذا فإن الوعد لن يستطيع معالجته.
ترجمة -وبتصرف- للفصل Error handling with promises من كتاب The JavaScript language
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.