JavaScript بعمق
معالجة الأخطاء والتنقيح: الفشل برشاقة وإيجاد العلل بسرعة
دليل عملي لمعالجة الأخطاء والتنقيح في JavaScript — try/catch/finally، وكائن Error وأصناف الأخطاء المخصّصة، والرمي الجيّد، ومعالجة الأخطاء غير المتزامنة، والمعالِجات العامة، وعُدّة تنقيح عملية في DevTools (نقاط التوقّف، ومكدّس الاستدعاء، وتوابع console، وخرائط المصدر) — مع تمارين عملية وحلولها.
مهارتان تفصلان بهدوء المحترفين عن المبتدئين: كتابة شيفرة تفشل برشاقة بدل أن تنهار، وإيجاد العلل بسرعة بدل التحديق في الشاشة. كلتاهما تعودان إلى فهم كيف تتدفّق الأخطاء عبر JavaScript وكيف تقود المنقّح. هذا المقال يغطّي معالجة الأخطاء عمدًا — المتزامنة وغير المتزامنة — وعُدّة DevTools عملية أقوى بكثير من نثر console.log. (تدفّق الأخطاء غير المتزامن يبني على الوعود وasync/await.)
الأخطاء ليست إخفاقات لشيفرتك — بل قناة. المهارة أن تقرّر، عند كل طبقة، إن كنت تعالج خطأً (تتعافى)، أو تحوّله (تضيف سياقًا)، أو تدعه ينتشر (مَن فوق يعرف أفضل). ابتلاع الأخطاء بصمت هو الخيار الخاطئ عالميًّا الوحيد.
try / catch / finally
البنية الأساسية. شيفرة try تعمل؛ إن رمى شيء، تقفز السيطرة إلى catch بالخطأ؛ وfinally يعمل مهما حدث:
try {
const data = JSON.parse(input); // يرمي عند JSON غير صالح
render(data);
} catch (err) {
console.error("فشل التحليل:", err.message);
showFallback();
} finally {
hideSpinner(); // يعمل دائمًا — نجاحًا أو فشلًا
}
finally للتنظيف الذي يجب أن يحدث بأي حال (إغلاق مورد، إخفاء مُحمِّل). أبقِ كتل try مركّزة حول العمليات التي قد ترمي فعلًا، بدل لفّ مساحات ضخمة من الشيفرة.
كائن Error
الخطأ المرميّ عادةً كائن Error (أو صنف فرعي) بخصائص مفيدة:
const err = new Error("شيء ما انكسر");
err.message; // "شيء ما انكسر"
err.name; // "Error"
err.stack; // سلسلة تتبّع المكدّس — أين رُمي
الأصناف الفرعية المدمجة تحمل معنى: TypeError (نوع خاطئ)، RangeError (خارج المدى)، SyntaxError (فشل تحليل)، ReferenceError (متغيّر غير معرّف). ارمِ دائمًا كائنات Error، لا سلاسل أبدًا — السلسلة بلا تتبّع مكدّس وتكسر الأدوات:
throw "سيّئ"; // ❌ بلا مكدّس، بلا اسم
throw new Error("مُدخَل سيّئ"); // ✅ خطأ سليم
أصناف الأخطاء المخصّصة
للإخفاقات الخاصّة بالمجال، اشتقّ من Error. هذا يتيح للمستدعين التقاط أنواع محدّدة والتفاعل مختلفًا:
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field; // سياق إضافي
}
}
function save(user) {
if (!user.email) throw new ValidationError("البريد مطلوب", "email");
}
try {
save({});
} catch (err) {
if (err instanceof ValidationError) {
highlightField(err.field); // عالِج هذا النوع تحديدًا
} else {
throw err; // أعد رمي أي شيء لا نفهمه
}
}
فحص instanceof هو الخطوة المفتاحية — يتيح لـ catch واحد التمييز بين مشكلة تحقّق وعلّة برمجية ومعالجة كلٍّ بما يناسبها.
معالجة الأخطاء غير المتزامنة
مع async/await، يعمل try/catch نفسه لأن رفضًا منتظَرًا يرمي:
async function load() {
try {
const res = await fetch("/api/data");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error(err);
throw err;
}
}
مع الوعود الخام، استخدم .catch() بدلًا منه. القاعدة الحاسمة: رفض وعد غير مُعالَج لا يلتقطه try/catch محيط إلا إن انتظرته بـ await — await منسيّ يدع الخطأ يفلت بصمت.
المعالِجات العامة (شبكة الأمان)
التقط ما يفلت، كي تسجّله إلى خدمة مراقبة بدل فقدانه:
// أخطاء متزامنة غير ملتقَطة
window.addEventListener("error", (e) => report(e.error));
// رفوض وعود غير مُعالَجة
window.addEventListener("unhandledrejection", (e) => report(e.reason));
هذه ملاذ أخير للملاحظة، لا بديل عن معالجة الأخطاء حيث تحدث. (في Node، المكافئان process.on("uncaughtException") و"unhandledRejection".)
التنقيح: أبعد من console.log
console.log يعمل، لكن المنقّح أسرع لأي شيء غير تافه. ضع نقطة توقّف (انقر رقم السطر في لوحة Sources بـ DevTools، أو اكتب debugger; في الشيفرة) فيتوقّف التنفيذ هناك، متيحًا لك:
- فحص كل متغيّر في النطاق في تلك اللحظة،
- قراءة مكدّس الاستدعاء — سلسلة استدعاءات الدوال التي أوصلتك هنا،
- التخطّي خطوة بخطوة: تخطَّ فوق، ادخل، اخرج،
- مراقبة التعابير ووضع نقاط توقّف شرطية (توقّف فقط حين
i === 42).
هذا يتفوّق على console.log لأنك ترى كل الحالة دفعة وتستطيع تغيير ما تفحصه دون إعادة التشغيل.
console أفضل
للطرفية أكثر بكثير من log:
console.error("..."); // أحمر، بتتبّع مكدّس
console.warn("..."); // أصفر
console.table(arrayOfObjects); // عرض جدوليّ — رائع للبيانات
console.group("الطلب"); /* ... */ console.groupEnd(); // سجلّات متداخلة
console.assert(x > 0, "يجب أن يكون x موجبًا"); // يسجّل فقط إن false
console.time("fetch"); /* ... */ console.timeEnd("fetch"); // المدّة
console.trace(); // اطبع مكدّس الاستدعاء هنا
console.table وconsole.group وحدهما يجعلان تنقيح شيفرة كثيفة البيانات أوضح بكثير من جدار أسطر log.
خرائط المصدر
شيفرتك المشحونة مجمَّعة ومصغَّرة — غير مقروءة. خرائط المصدر (source maps) تربطها عائدةً بملفّاتك الأصلية، فتتحاذى نقاط التوقّف وتتبّعات المكدّس مع الشيفرة التي كتبتها فعلًا. يولّدها مُجمِّعك؛ فقط تأكّد من تفعيلها في التطوير (ورفعها إلى أداة مراقبة الأخطاء لتتبّعات الإنتاج).
الأخطاء الشائعة
- ابتلاع الأخطاء —
catch {}فارغ يخفي العلّة بدل إظهارها. - رمي سلاسل بدل كائنات
Error، ففقدان تتبّع المكدّس. - الالتقاط بعرض كبير ومعاملة علّة برمجية كفشل متوقّع.
- نسيان
await، فيفلت رفض وعد منtry/catchالمحيط. - تسجيل خطأ والمتابعة كأن شيئًا لم يحدث، تاركًا حالة فاسدة.
- ترك عبارات
console.log/debuggerفي الشيفرة المشحونة. - الاعتماد على المعالِجات العامة فقط بدل معالجة الأخطاء قرب حدوثها.
- إظهار رسائل خطأ خام للمستخدمين بدل بديل ودود (وتسجيل التفصيل).
تمارين
جرّب كلًّا منها قبل فتح الحل.
تمرين 1 — احرس تحليلًا
حلّل سلسلة JSON بأمان، مُرجِعًا null (لا انهيارًا) إن كانت غير صالحة.
إظهار الحل
function safeParse(str) {
try {
return JSON.parse(str);
} catch {
return null; // JSON غير صالح → بديل رشيق
}
}
JSON.parse يرمي عند مُدخَل سيّئ؛ وcatch يحوّل ذلك إلى null يستطيع المستدعي فحصه، بدل انهيار غير ملتقَط.
تمرين 2 — خطأ مخصّص
أنشئ NotFoundError وارمِه من getUser لا يجد مستخدمًا.
إظهار الحل
class NotFoundError extends Error {
constructor(id) {
super(`المستخدم ${id} غير موجود`);
this.name = "NotFoundError";
}
}
function getUser(id) {
const user = db.find(id);
if (!user) throw new NotFoundError(id);
return user;
}
الاشتقاق من Error يتيح للمستدعين catch (e) { if (e instanceof NotFoundError) ... } ومعاملة "غير موجود" مختلفةً عن الإخفاقات الأخرى.
تمرين 3 — عالِج فشلًا غير متزامن
لُفّ fetch كي يُلتقَط خطأ شبكة أو HTTP ويُسجَّل، ثم يُعاد رميه.
إظهار الحل
async function getData(url) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error(`فشل getData(${url}):`, err);
throw err; // دع المستدعي يقرّر ماذا يفعل
}
}
try/catch يلتقط رفض الشبكة ورمي HTTP اليدويّ؛ وإعادة الرمي تُبقي المستدعي مُطّلِعًا بدل إرجاع undefined بصمت.
النموذج الذهني الذي تحتفظ به
عامِل الأخطاء كقناة، لا ككارثة. استخدم try/catch/finally حول العمليات التي قد ترمي فعلًا، وارمِ دائمًا كائنات Error حقيقية (اشتقّها للإخفاقات الخاصّة بالمجال كي يميّزها المستدعون بـ instanceof)، وعند كل طبقة قرّر أن تعالج، أو تضيف سياقًا، أو تعيد الرمي — لا تبتلع بصمت أبدًا. الأخطاء غير المتزامنة تتدفّق عبر try/catch نفسه ما دمت تنتظر بـ await، مع .catch() للوعود الخام ومعالِجات عامة كشبكة أمان أخيرة. لإيجاد العلل، ارتقِ من console.log إلى المنقّح — نقاط التوقّف، ومكدّس الاستدعاء، والتخطّي، والمراقبات تُريك كل الحالة دفعة — واتّكئ على توابع console الأغنى وخرائط المصدر. شيفرة تفشل برشاقة ومطوّر يقود المنقّح: ذلك المزيج هو ما يجعل العلل رخيصة بدل مرهوبة.