EduFlow
أساسيات JavaScript
⁦0%⁩

الوحدة 5 · كتابة برامج حقيقية

الأخطاء وتصحيحها باستخدام DevTools

17 دقراءة 4 دبيئة تجريب الكود

الدرس في لمحة

تقرأ رسائل الأخطاء وآثار المكدّس (stack traces)، وترمي الأخطاء وتلتقطها عن قصد بأنواع أخطاء مخصّصة، وتجد الأخطاء البرمجية بشكل منهجي باستخدام نقاط التوقّف ومصحّح الأخطاء في DevTools.

  1. 1قراءة الخطأ
  2. 2رمي الأخطاء عن قصد
  3. 3مصحّح الأخطاء: توقّف وانظر
  4. 4طريقة تصلح لكل خطأ

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

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

قراءة الخطأ

حين تعجز JavaScript عن الاستمرار، فإنها ترمي (throw) خطأً، وإن لم يلتقطه شيء، يعرض الـ console شيئًا كهذا:

Text
Uncaught TypeError: Cannot read properties of undefined (reading 'amount')
    at totalCents (expenses.js:12:31)
    at renderSummary (ui.js:40:17)
    at HTMLFormElement.<anonymous> (main.js:27:5)

اقرأه في ثلاثة أجزاء. النوع، TypeError، يقول ما صنف المشكلة: قيمة كان نوعها خاطئًا بالنسبة لما فعلته بها. والرسالة تقول ماذا بالضبط: شيء ما قبل .amount كان undefined. وأثر المكدّس (stack trace) يسرد سلسلة استدعاءات الدوال التي أوصلت إلى هناك، الأحدث أولًا: فشلت totalCents في السطر 12 من expenses.js، وكانت قد استدعتها renderSummary، التي استدعاها معالج الإرسال في النموذج. وفي DevTools، كل موقع منها رابط يأخذك مباشرة إلى ذلك السطر.

الأنواع التي ستصادفها أكثر من غيرها:

النوع يعني عادةً
ReferenceError اسم غير موجود: خطأ إملائي، أو استخدام قبل التصريح
TypeError نوع خاطئ من القيم: استدعاء شيء ليس دالة، أو القراءة من undefined
SyntaxError تعذّر تحليل الكود: قوس أو علامة تنصيص أو فاصلة مفقودة
RangeError رقم خارج المسموح، مثل toFixed(200)

رمي الأخطاء عن قصد

يمكنك أن ترمي الأخطاء بنفسك، وينبغي أن تفعل حين يُعطى كودك شيئًا لا يستطيع التعامل معه بشكل معقول:

JavaScript
function parseAmount(text) {
  const dollars = Number(text);
  if (text.trim() === "" || !Number.isFinite(dollars) || dollars <= 0) {
    throw new Error(`Invalid amount: "${text}"`);
  }
  return Math.round(dollars * 100);
}

try {
  console.log(parseAmount("12.20"));
  console.log(parseAmount("twelve"));
  console.log("this line never runs");
} catch (error) {
  console.log("Caught:", error.message);
} finally {
  console.log("finally always runs");
}

throw توقف الدالة فورًا وتصعد في مكدّس الاستدعاءات حتى تلتقطها كتلة try/catch ما. وإن لم تلتقطها أي كتلة، تصبح خطأً غير ملتقط في الـ console. أما finally فتُنفَّذ سواء رُمي شيء أم لا، وهي المكان المناسب للتنظيف، مثل إخفاء مؤشّر التحميل.

أنواع أخطاء مخصّصة

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

JavaScript
class ValidationError extends Error {
  constructor(field, message) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
  }
}

function addFromForm(label, amountText) {
  if (label.trim() === "") {
    throw new ValidationError("label", "Enter what you spent money on.");
  }
  return { label: label.trim(), amount: Math.round(Number(amountText) * 100) };
}

try {
  addFromForm("  ", "4.50");
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`Show next to ${error.field}: ${error.message}`);
  } else {
    throw error; // not ours to handle
  }
}

extends Error تجعل ValidationError نوعًا من Error له أثر مكدّس، وsuper(message) تنفّذ الـ constructor الخاص بالصنف الأب، والخاصية الإضافية field تخبر النموذج أين يعرض الرسالة. وكتلة catch لا تتعامل إلا مع ما تفهمه، وتعيد رمي كل ما عداه، فتظل الأخطاء البرمجية الحقيقية تطفو إلى السطح.

مصحّح الأخطاء: توقّف وانظر

console.log أداة أولى جيدة، لكنها لا تُريك إلا ما خطر لك أن تطبعه. أما مصحّح الأخطاء (debugger) في DevTools فيوقف برنامجك مؤقتًا عند سطر ما ويُريك كل شيء.

في Chrome أو Edge، افتح DevTools، واذهب إلى لوحة Sources، وافتح ملفك، وانقر على رقم سطر. هذا يضع نقطة توقّف (breakpoint). وفي المرة التالية التي يوشك فيها هذا السطر أن يُنفَّذ، يتوقّف كل شيء مؤقتًا، وتستطيع:

  • أن تمرّر المؤشّر فوق أي متغيّر لترى قيمته، أو تقرأها كلها في قسم Scope؛
  • أن تقرأ قسم Call Stack لترى كيف وصلت إلى هنا؛
  • أن تتخطّى (step over: نفّذ هذا السطر وتوقّف عند التالي)، أو تدخل (step into: اتبع استدعاء الدالة إلى داخلها)، أو تخرج (step out: أكمل هذه الدالة)؛
  • أن تكتب أي تعبير في الـ Console أثناء التوقّف، مستخدمًا القيم المتوقّفة.

انقر بالزر الأيمن على رقم سطر لتضيف نقطة توقّف مشروطة، مثل expense.amount > 100000، لا تتوقّف إلا حين يكون الشرط صحيحًا. ويمكنك أيضًا أن تكتب التعليمة debugger; في كودك: فحين تكون DevTools مفتوحة، يتوقّف التنفيذ هناك. احذفها قبل أن تُجري commit.

دورة من خمس خطوات: أعِد إنتاج الخطأ بشكل موثوق، واقرأ الخطأ أو صِف المخرجات الخاطئة، وضع فرضية، وافحص القيم بالطباعة أو بنقاط التوقّف، ثم أصلح وتحقّق، وكرّر إن بقي الخطأ. أعِد الإنتاج اقرأ افترض افحص أصلح وتحقّق ما زال معطّلًا: عُد إلى القراءة
حلقة تصحيح تنجح مع كل خطأ برمجي

طريقة تصلح لكل خطأ

  1. أعِد إنتاجه بشكل موثوق. “أحيانًا يكون المجموع خاطئًا” تصبح “بعد حذف المصروف الأول، يكون المجموع خاطئًا”.
  2. اقرأ الخطأ، أو صِف بدقة ما توقّعته وما حصلت عليه.
  3. افترض: ما الشيء الواحد الذي يفسّر ذلك؟
  4. افحص القيم عند النقطة التي تشتبه بها، بنقطة توقّف أو بالطباعة، وجد أول مكان يختلف فيه الواقع عن توقّعك.
  5. أصلح السبب هناك، ثم تحقّق بالخطوات نفسها بالضبط من الخطوة 1.

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

صار لديك الآن كل قطعة يحتاجها Pocket. وفي الدرس التالي تجمعها في التطبيق المكتمل.

أهم النقاط

  • رسالة الخطأ تسمّي نوع الخطأ، وتقول ما الذي حدث، وأثر المكدّس (stack trace) الخاص بها يشير إلى الملف والسطر حيث وقع.
  • ارمِ Error حين يعجز كودك عن الاستمرار بشكل معقول؛ والتقطه فقط حيث تستطيع فعل شيء مفيد، وأعِد رمي كل ما لم تتوقّعه.
  • صنف خطأ مخصّص (class ValidationError extends Error) يتيح للمُستدعين التمييز بين المشاكل المتوقّعة والأخطاء البرمجية الحقيقية باستخدام instanceof.
  • نقطة التوقّف (breakpoint) توقف كودك مؤقتًا عند سطر ما كي تفحص كل متغيّر وتتقدّم فيه سطرًا سطرًا؛ وdebugger; تضع واحدة من داخل الكود.
  • صحّح الأخطاء بالأدلة لا بالتخمين؛ أعِد إنتاج الخطأ، وجد أول سطر يختلف فيه الواقع عن توقّعك، ثم أصلحه.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

TypeError: expenses.map is not a function عند renderExpenses (ui.js:14). ما الخطوة الأولى الأنفع؟

السؤال 2 من 3

كتلة catch في دالة الحفظ في Pocket مكتوبة هكذا: catch (error) {}. ما المشكلة؟

السؤال 3 من 3

تريد أن تعرف لماذا يكون المجموع خاطئًا فقط عند المصروف رقم 40 في حلقة. أيّ ميزة في DevTools أنسب؟

تدريب

بيئة تجريب الكود

ارمِ ما يستطيع المستخدمون إصلاحه، وأعِد رمي الباقي

  1. أكمل ValidationError بحيث تكون قيمة name في الـ instances هي "ValidationError"، وتكون لها خاصية field.
  2. اجعل parseAmount(text) تُرجع سنتات صحيحة، أو ترمي ValidationError بالحقل "amount" حين يكون النص فارغًا، أو ليس رقمًا منتهيًا، أو ليس أكبر من 0.
  3. أصلح tryAdd(list, label, amountText). مع الإدخال الصالح تُرجع { ok: true, list } وقد أُضيف إليها المصروف الجديد (بتسمية مشذّبة، والمبلغ بالسنتات). والتسمية الفارغة ترمي ValidationError بالحقل "label". وأخطاء التحقّق تتحوّل إلى { ok: false, field, message }. وأي خطأ آخر يجب أن يُعاد رميه، لا أن يُبتلع.
// 1. An error type for problems the user can fix
class ValidationError extends Error {
  constructor(field, message) {
    super(message);
    // set name and field here
  }
}

// 2. Return whole cents, or throw a ValidationError for the "amount" field
function parseAmount(text) {
  return Math.round(Number(text) * 100);
}

// 3. Return { ok: true, list } with the new expense added, or
//    { ok: false, field, message } for a ValidationError.
//    Any other error must NOT be swallowed.
function tryAdd(list, label, amountText) {
  try {
    const expense = { label: label.trim(), amount: parseAmount(amountText) };
    return { ok: true, list: [...list, expense] };
  } catch (error) {
    return { ok: false };
  }
}

console.log(tryAdd([], "Tea", "abc"));
جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

الـ console

(لا مخرجات)

الفحوص

  • ValidationError هو Error له name وfield
  • parseAmount تُرجع سنتات صحيحة مع النص الصالح
  • parseAmount ترمي ValidationError مع المدخلات الفارغة أو غير الرقمية أو الصفرية أو السالبة أو اللانهائية
  • tryAdd تضيف المصروف الصالح
  • tryAdd تُرجع { ok: false, field, message } مع المدخلات غير الصالحة
  • tryAdd تعيد رمي الأخطاء التي ليست من نوع ValidationError