DanLevy.net

وعود مكسورة؟

تجاهل الأخطاء، فقدان النتائج...

Hero image for وعود مكسورة؟

هل وعود JavaScript معطلة؟

في الزمن السابق

أحد أكثر الخرافات شيوعًا حول الوعود (Promises) هو القصور المزعوم في معالجة الأخطاء.

منذ سنوات عديدة كانت الوعود فعلاً سيئة جدًا مع الأخطاء. تم بذل الكثير من العمل لإصلاح ذلك.

وها هو ذا، تم إصلاحه، بل وانتشر على نطاق واسع.

ابتهج الناس

وللأسف، لم يلاحظ البعض ذلك.

الزمن الحالي

لا تزال الخرافة قائمة، وأراها في كل مكان: مقالات شائعة على medium، على dzone، وعدة مصادر أخرى.

سأعترف، حتى المصادر والوثائق “الرسمية” تقدم في الغالب أمثلة واهية وعادات سيئة. غالبًا ما تُستخدم هذه “لإثبات” القضية ضد الوعود. بل إن البعض يقترح “علاجات” تجعل الأمور أسوأ بكثير. (ملاحظة: تمت إزالة الرابط)



قواعد لتجنب المشاكل

  1. الوعود تحتاج إلى شيء تتشبث به
    • دائمًا return من دوالك.
  2. استخدم مثيلات Error حقيقية
    • دائمًا استخدم مثيلات Error.
  3. تعامل مع الأخطاء حيثما كان ذلك منطقيًا
    • دائمًا استخدم .catch()، على الأقل مرة واحدة.
  4. أضف الوضوح باستخدام الدوال المسماة 🦄✨
    • فضل الدوال المسماة.

#1 الوعود تحتاج إلى شيء تتشبث به

من الضروري أن تقوم دائمًا بـ return من دوالك.

تتبع دوال الاستدعاء للوعود نمطًا معينًا في .then(callback) و .catch(callback).

يتم تمرير كل قيمة معادة إلى دالة الاستدعاء التالية لـ .then().

function addTen(number) {
return number + 10;
}
Promise.resolve(10) // 10
.then(addTen) // 20
.then(addTen) // 30
.then(addTen) // 40
.then(console.log) // logs "40"

مكافأة “العودة دائمًا”: الكود أسهل بكثير في اختبار الوحدة.

سؤال: كم عدد حالات الوعد المميزة (تم الحل والرفض) التي تم إنشاؤها؟

سؤال: كم عدد الوعود التي تم إنشاؤها في المثال السابق؟

#2 استخدم مثيلات Error الحقيقية

لدى JavaScript سلوك مثير للاهتمام حول الأخطاء (ينطبق على الكود غير المتزامن و المتزامن.)

[انظر المثال في repl.it: throwing errors in javascript] رمي الأخطاء في جافا سكريبت

للحصول على تفاصيل مفيدة حول رقم السطر ومكدس الاستدعاءات، يجب عليك استخدام مثيلات Error. رمي السلاسل النصية لا يعمل كما في Python أو Ruby.

بينما يبدو أن JavaScript تتعامل مع throw "string"، كما سترى السلسلة في معالج catch الخاص بك. ومع ذلك، فإن البيانات هي كل ما ستراه*. لن يتم تضمين أي إطارات مكدس سابقة.

أمثلة صحيحة new Error:

throw new Error('message') // ✅
Promise.reject(new Error('message')) // ✅
throw Error('message') // ✅
Promise.reject(Error('message')) // ✅

فيما يلي أنماط مضادة شائعة:

throw 'error message' // ❌
Promise.reject(-42) // ❌

#3 تعامل مع الأخطاء حيثما يكون ذلك منطقيًا

توفر الـ Promises طريقة أنيقة للتعامل مع الأخطاء باستخدام .catch(). إنها في الأساس نوع خاص من .then() - حيث يتم التعامل مع أي أخطاء من .then() السابقة. دعنا نلقي نظرة على مثال…

Promise.resolve(42)
.then(() => 'hello')
.catch(() => console.log('will not get hit'))
.then(() => throw new Error('totes fail'))
.catch(() => console.log('WILL get hit'))

بينما قد يبدو .catch() مثل معالج أحداث DOM (مثل click، keypress). فإن موضعه مهم، حيث يمكنه فقط ‘التقاط’ الأخطاء التي تم رميها فوقه.

تجاوز الأخطاء أمر تافه نسبيًا قم بإرجاع قيمة غير خطأ في رد الاتصال .catch()، وسيتحول سلسلة الـ Promise إلى تشغيل ردود الاتصال .then() بالتسلسل. (بشكل فعال.)

حاول متابعة تسلسل المثال التالي:

Promise.resolve(42)
.then(() => 'hello')
.then(() => throw new Error('totes fail'))
.catch(() => {
return 99
})
.then(num => num + 1)
.then(console.log) // expected output: 100

التسلسل هو ما يهم فهمه.

على الرغم من أنه مثال سخيف، إلا أنه مصمم لتوضيح كيفية تدفق الأخطاء والبيانات في الـ Promises.

فيما يلي مخطط للتسلسل:

  1. 42 هي القيمة الأولية.
  2. يتم دائمًا إرجاع hello بواسطة الطريقة التالية.
  3. نتجاهل القيمة السابقة، ونرمي خطأ برسالة 'totes fail'.
  4. يتدخل .catch() في الخطأ، ويعيد بدلاً من ذلك 99 والتي سيتم التعامل معها بواسطة أي .then() لاحق.
  5. نزيد num، ونعيد 100.
  6. تستقبل الطريقة console.log القيمة 100 وتطبعها! :tada:

سؤال: ماذا يحدث عندما يكون هناك .catch() متتاليان؟ هل يمكن للثاني أن يعمل أبدًا؟ هل يمكنك التفكير في حالة استخدام؟

سؤال: كيف يمكن لـ .catch() تجاهل الأخطاء؟ كيف تمنع الأخطاء من إجبار الخروج المبكر من Promise.all؟

#4 أضف الوضوح باستخدام الدوال المسماة 🦄✨

قارن قابلية القراءة بين المثالين التاليين:

مجهول:

Promise.resolve(10) // 10
.then(x => x * 2) // 20
.then(x => x / 4) // 5
.then(x => x * x) // 25
.then(x => x.toFixed(2)) // "25.00"
.then(x => console.log(x)) // expected output: "25.00"

مسمى:

Promise.resolve(10) // 10
.then(double) // 20
.then(quarter) // 5
.then(square) // 25
.then(format) // "25.00"
.then(log) // expected output: "25.00"
const double = x => x * 2
const quarter = x => x / 4
const square = x => x * x
const format = x => x.toFixed(2)
const log = x => console.log(x)

مكافأة:

متوافقة مع دوال المصفوفات!!!

يمكنك إعادة استخدام دوالك المسماة مع أصدقائنا من Array.prototype. بما في ذلك .map() و .filter() و .every() و .some() و .find()!

خطوط أنابيب التجميع #للنصر:

// إنه نفس الشيء :انفجار_ذهني:
[10, 20] // [ 10, 20 ]
.map(double) // [ 20, 40 ]
.map(quarter) // [ 5, 10 ]
.map(square) // [ 25, 100 ]
.map(format) // [ "25.00", "100.00" ]
.map(log) // expected 2 lines of output: "25.00", "100.00"

وإذا كنت لا تريد البرمجة بهذا الأسلوب الخطي… حسنًا، لديك دوال بسيطة!

يمكنك استخدامها بالطريقة التي تناسبك:

// نمط التداخل
// ❌ من فضلك لا تفعل هذا، ولكن
const result = format(square(quarter(double(10))))
log(result)
// الناتج المتوقع: "25.00"

لماذا يُعتبر تداخل الدوال نمطًا سيئًا؟

  1. غير قابل للقراءة لعدد كبير من الأشخاص
  2. لا تُظهر فروق git بسهولة من غيّر ماذا
  3. يصعب تصحيح الأخطاء أو تسجيل السجلات من منتصف الدوال المتداخلة