معالجة الأخطاء وتتبعها — JavaScript الأساسيات
معالجة الأخطاء وتتبعها
البرامج الحقيقية تواجه مدخلات خاطئة، وشبكة مقطوعة، وبيانات ناقصة. الفرق بين برنامج احترافي وآخر هش ليس "عدم وجود أخطاء" بل التعامل معها: رسالة واضحة للمستخدم بدل صفحة بيضاء.
أنواع الأخطاء الشائعة
| النوع | متى | مثال |
|---|---|---|
| SyntaxError | الكود نفسه غير صحيح نحوياً (لا يعمل أصلاً) | قوس ناقص |
| ReferenceError | استخدام متغير غير معرّف | console.log(userNmae) |
| TypeError | عملية على نوع لا يدعمها | null.length، استدعاء ما ليس دالة |
| RangeError | قيمة خارج المدى المسموح | new Array(-1) |
const user = null;
try {
console.log(user.name); // TypeError: Cannot read properties of null
} catch (error) {
console.log(error.name); // TypeError
console.log(error.message); // Cannot read properties of null (reading 'name')
}اقرأ رسالة الخطأ كاملة: نوعه، ورسالته، ورقم السطر. أغلب الأخطاء تشرح نفسها.
try و catch و finally
function parseSettings(text) {
try {
const settings = JSON.parse(text);
return settings;
} catch (error) {
console.log("إعدادات تالفة، نستخدم الافتراضية:", error.message);
return { theme: "light" };
} finally {
console.log("انتهت محاولة قراءة الإعدادات"); // يُنفَّذ دائماً
}
}
console.log(parseSettings('{"theme":"dark"}'));
console.log(parseSettings("{خطأ"));- try: الكود الذي قد يفشل.
- catch (error): يُنفَّذ فقط عند حدوث خطأ؛ error كائن فيه name و message و stack.
- finally: يُنفَّذ دائماً — للتنظيف (إخفاء مؤشر التحميل، إغلاق مورد).
رمي الأخطاء: throw
دالتك تستطيع الإبلاغ عن مشكلة لمن استدعاها:
function withdraw(balance, amount) {
if (typeof amount !== "number" || amount <= 0) {
throw new TypeError("المبلغ يجب أن يكون رقماً موجباً");
}
if (amount > balance) {
throw new RangeError("الرصيد غير كافٍ");
}
return balance - amount;
}
try {
withdraw(100, 500);
} catch (error) {
console.log(`${error.name}: ${error.message}`); // RangeError: الرصيد غير كافٍ
}ارمِ دائماً كائن Error (أو مشتقاً منه)، لا نصاً: throw "خطأ" يفقد stack الذي يدلك على مكان المشكلة.
أخطاء مخصصة
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function validateUser(user) {
if (!user.email?.includes("@")) throw new ValidationError("email", "بريد غير صالح");
return true;
}
try {
validateUser({ email: "sara.example.com" });
} catch (error) {
if (error instanceof ValidationError) {
console.log(`الحقل ${error.field}: ${error.message}`); // نعرضه بجانب الحقل
} else {
throw error; // خطأ لم نتوقعه: لا نبتلعه
}
}لا تبتلع الأخطاء: catch (e) {} الفارغ يخفي المشاكل ويجعل التتبع مستحيلاً. التقط ما تعرف كيف تعالجه، وأعد رمي الباقي.
الأخطاء في الكود غير المتزامن
مع async/await تعمل try/catch كالمعتاد:
async function loadUser(id) {
try {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
if (!response.ok) throw new Error(`فشل الطلب: ${response.status}`); // fetch لا يرمي عند 404!
return await response.json();
} catch (error) {
console.error("تعذر تحميل المستخدم:", error.message);
return null;
}
}⚠️ fetch لا يرمي خطأ عند الحالة 404 أو 500 — فقط عند فشل الشبكة. افحص response.ok دائماً.
مع Promises بدون await استخدم .catch(). الـ Promise المرفوض بلا معالجة يظهر في الطرفية كـ "Uncaught (in promise)".
التتبع (Debugging)
1. اقرأ الرسالة ورقم السطر في الطرفية (Console)، وانقر على اسم الملف للقفز للسطر.
2. console.log ذكياً: console.log({ user, total }) يطبع الأسماء مع القيم. و console.table(array) للمصفوفات.
3. نقاط التوقف (Breakpoints): في تبويب Sources انقر على رقم السطر؛ يتوقف التنفيذ هناك وترى قيم كل المتغيرات، ثم تتقدم سطراً سطراً. أو اكتب debugger; في الكود.
4. صغّر المشكلة: احذف الأجزاء حتى تبقى أصغر نسخة تُظهر الخطأ.
متى نستخدم try/catch؟
- حول العمليات التي قد تفشل لأسباب خارجية: JSON.parse لبيانات من مصدر خارجي، طلبات الشبكة، localStorage (قد يكون ممتلئاً أو محظوراً).
- ليس بديلاً عن الفحص العادي: إذا كان يمكن فحص الشرط بـ if فافحصه.
تمرين
اكتب دالة safeDivide(a, b) ترمي TypeError إذا لم يكن المعاملان رقمين و RangeError عند القسمة على صفر. استدعها بثلاث حالات داخل try/catch واطبع نوع كل خطأ. ثم اكتب loadSettings() تقرأ JSON من localStorage وتعيد إعدادات افتراضية إذا كان تالفاً.
// خطأ مخصص يحمل اسم الحقل
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function validateSignup({ name, email, age }) {
if (!name || name.trim().length < 2) throw new ValidationError("name", "الاسم قصير جداً");
if (!email?.includes("@")) throw new ValidationError("email", "بريد غير صالح");
if (!Number.isInteger(age) || age < 13) throw new ValidationError("age", "العمر يجب أن يكون 13 فأكثر");
return true;
}
const attempts = [
{ name: "سارة", email: "sara@example.com", age: 20 },
{ name: "ع", email: "ali@example.com", age: 20 },
{ name: "عمر", email: "omar.example.com", age: 20 },
{ name: "ليلى", email: "layla@example.com", age: 10 },
];
for (const data of attempts) {
try {
validateSignup(data);
console.log(`✅ ${data.name}: بيانات صحيحة`);
} catch (error) {
if (error instanceof ValidationError) {
console.log(`❌ ${data.name}: [${error.field}] ${error.message}`);
} else {
throw error; // خطأ غير متوقع: لا نخفيه
}
}
}
// JSON تالف مع قيمة افتراضية و finally
function parseSettings(text) {
try {
return JSON.parse(text);
} catch (error) {
console.log(`⚠️ ${error.name}: نستخدم الإعدادات الافتراضية`);
return { theme: "light", fontSize: 16 };
} finally {
console.log("تمت محاولة قراءة الإعدادات");
}
}
console.log(parseSettings('{"theme":"dark","fontSize":18}'));
console.log(parseSettings("{تالف"));
// الأخطاء مع async/await
async function riskyTask(shouldFail) {
await new Promise((resolve) => setTimeout(resolve, 100));
if (shouldFail) throw new Error("فشلت المهمة");
return "نجحت المهمة";
}
(async () => {
for (const fail of [false, true]) {
try {
console.log(await riskyTask(fail));
} catch (error) {
console.log("التقطنا:", error.message);
}
}
})();اختبر فهمك