دليل تعلم جافاسكربت التعامل مع أخطاء الوعود في جافاسكربت


صفا الفليج

تُعدّ سلاسل الوعود ممتازة في التعامل مع الأخطاء، فمتى رُفض الوعد ينتقل سير التحكّم إلى أقرب دالة تتعامل مع حالة الرفض، وهذا عمليًا يسهّل الأمور كثيرًا.

فمثلًا نرى في الشيفرة أسفله أنّ العنوان المرّر إلى 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





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن