أساسيات JavaScript

معالجة الأخطاء: try و catch و finally

45 دقيقة الدرس 30 من 60

فهم أخطاء وقت التشغيل

مهما كنت حريصا في كتابة الكود ستحدث أخطاء. سيدخل المستخدمون مدخلات غير متوقعة وستفشل طلبات الشبكة وستعيد واجهات برمجة التطبيقات بيانات مشوهة وستختفي الملفات. الفرق بين التطبيق الهش والتطبيق المتين هو كيف يتعامل مع هذه الأخطاء الحتمية. يوفر JavaScript آلية معالجة أخطاء قوية من خلال كتل try وcatch وfinally التي تسمح لك بالتعافي من الأخطاء بأناقة بدلا من ترك تطبيقك بالكامل ينهار.

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

مثال: أنواع الأخطاء

// خطأ بناء الجملة -- يُكتشف قبل التنفيذ (لا يمكن اصطياده بـ try/catch)
// const x = ; // SyntaxError: Unexpected token

// خطأ وقت التشغيل -- يحدث أثناء التنفيذ (يمكن اصطياده بـ try/catch)
const obj = undefined;
// obj.name; // TypeError: Cannot read properties of undefined

// خطأ منطقي -- لا يُرمى خطأ لكن النتيجة خاطئة
function calculateArea(width, height) {
    return width + height; // خلل: يجب أن تكون width * height
}
console.log(calculateArea(5, 3)); // 8 (خطأ، يجب أن تكون 15)

تعليمة try...catch

تعليمة try...catch هي أساس معالجة الأخطاء في JavaScript. تقوم بلف الكود الذي قد يرمي خطأ داخل كتلة try وإذا حدث خطأ ينتقل التنفيذ فورا إلى كتلة catch بدلا من تعطل البرنامج.

مثال: try...catch الأساسية

try {
    // كود قد يرمي خطأ
    const data = JSON.parse('{"name": "Alice"}');
    console.log(data.name); // "Alice"
} catch (error) {
    // هذه الكتلة تعمل فقط إذا حدث خطأ في كتلة try
    console.log('حدث خطأ:', error.message);
}

console.log('البرنامج يستمر في العمل!');

// عندما يحدث خطأ فعلا:
try {
    const data = JSON.parse('invalid json string');
    console.log(data); // هذا السطر لا يُنفذ أبدا
} catch (error) {
    console.log('تم اصطياد خطأ!');
    console.log('الرسالة:', error.message);
    // "Unexpected token i in JSON at position 0"
}

console.log('لا يزال يعمل بعد الخطأ!');
ملاحظة: تعليمة try...catch تصطاد فقط أخطاء وقت التشغيل في الكود المتزامن داخل كتلة try. لا تصطاد أخطاء بناء الجملة (التي تمنع الكود من العمل أصلا) أو الأخطاء في استدعاءات غير متزامنة مثل setTimeout. لمعالجة الأخطاء غير المتزامنة مع Promises و async/await ستتعلم أنماطا إضافية في الدروس اللاحقة.

كائن الخطأ

عندما يرمي JavaScript خطأ ينشئ كائن Error بثلاث خصائص مهمة تساعدك على فهم ما حدث وأين.

مثال: خصائص كائن الخطأ

try {
    const result = undeclaredVariable + 10;
} catch (error) {
    // كائن الخطأ له ثلاث خصائص رئيسية:

    // 1. name -- نوع الخطأ
    console.log('الاسم:', error.name);
    // "ReferenceError"

    // 2. message -- وصف مقروء للبشر
    console.log('الرسالة:', error.message);
    // "undeclaredVariable is not defined"

    // 3. stack -- تتبع مكدس الاستدعاءات (للتصحيح)
    console.log('المكدس:', error.stack);
    // "ReferenceError: undeclaredVariable is not defined
    //     at <anonymous>:2:20"
}

// يمكنك أيضا التحقق من نوع الخطأ
try {
    null.toString();
} catch (error) {
    console.log(error instanceof TypeError); // true
    console.log(error instanceof ReferenceError); // false
}

معامل catch

كتلة catch تستقبل كائن الخطأ كمعامل. يمكنك تسمية هذا المعامل بأي شيء رغم أن error وerr وe هي الاصطلاحات الأكثر شيوعا. في JavaScript الحديث (ES2019+) يمكنك أيضا حذف المعامل تماما إذا لم تحتج معلومات الخطأ.

مثال: أشكال معامل Catch

// القياسي: معامل خطأ مسمى
try {
    JSON.parse('bad json');
} catch (error) {
    console.log(error.message);
}

// الشكل المختصر الشائع
try {
    JSON.parse('bad json');
} catch (err) {
    console.log(err.message);
}

// حرف واحد (شائع في الكود المختصر)
try {
    JSON.parse('bad json');
} catch (e) {
    console.log(e.message);
}

// ربط catch الاختياري (ES2019+) -- لا حاجة لمعامل
try {
    JSON.parse('bad json');
} catch {
    console.log('فشل تحليل JSON، استخدام القيم الافتراضية.');
}

كتلة finally

كتلة finally تعمل مهما حدث -- سواء اكتملت كتلة try بنجاح أو تم اصطياد خطأ. هذا يجعلها مثالية لعمليات التنظيف مثل إغلاق الاتصالات وتحرير الموارد أو إخفاء مؤشرات التحميل.

مثال: try...catch...finally

function processData(jsonString) {
    console.log('بدء معالجة البيانات...');

    try {
        const data = JSON.parse(jsonString);
        console.log('تم تحليل البيانات بنجاح:', data);
        return data;
    } catch (error) {
        console.log('خطأ في تحليل البيانات:', error.message);
        return null;
    } finally {
        // هذا يعمل دائما حتى بعد تعليمة return
        console.log('اكتملت المعالجة. جاري التنظيف...');
    }
}

// حالة ناجحة
processData('{"status": "ok"}');
// "بدء معالجة البيانات..."
// "تم تحليل البيانات بنجاح: {status: 'ok'}"
// "اكتملت المعالجة. جاري التنظيف..."

// حالة خطأ
processData('not json');
// "بدء معالجة البيانات..."
// "خطأ في تحليل البيانات: Unexpected token ..."
// "اكتملت المعالجة. جاري التنظيف..."

مثال: استخدام finally في العالم الحقيقي

// محاكاة اتصال قاعدة بيانات
function queryDatabase(query) {
    let connection = null;

    try {
        connection = openConnection(); // قد يرمي خطأ
        const result = connection.execute(query); // قد يرمي خطأ
        return result;
    } catch (error) {
        console.error('فشل استعلام قاعدة البيانات:', error.message);
        return null;
    } finally {
        // أغلق الاتصال دائما حتى لو حدث خطأ
        if (connection) {
            connection.close();
            console.log('تم إغلاق الاتصال.');
        }
    }
}

// نمط مؤشر التحميل
function fetchUserData(userId) {
    showLoadingSpinner(); // أظهر المؤشر قبل البدء

    try {
        const user = getUserFromAPI(userId);
        displayUserProfile(user);
    } catch (error) {
        displayErrorMessage('تعذر تحميل ملف المستخدم.');
    } finally {
        hideLoadingSpinner(); // أخفِ المؤشر دائما عند الانتهاء
    }
}
نصيحة احترافية: كتلة finally تُنفذ حتى لو تم الوصول إلى تعليمة return داخل كتلة try أو catch. لكن إذا وضعت return في كتلة finally نفسها فسوف تتجاوز القيمة المعادة من try أو catch. تجنب إعادة القيم من finally لمنع الارتباك.

رمي أخطاء مخصصة

يمكنك رمي أخطائك الخاصة باستخدام تعليمة throw. هذا ضروري لفرض قواعد العمل والتحقق من المدخلات وإنشاء رسائل خطأ ذات معنى تساعد المستدعين على فهم ما حدث.

مثال: رمي الأخطاء

// throw new Error(message) ينشئ ويرمي كائن Error
function divide(a, b) {
    if (typeof a !== 'number' || typeof b !== 'number') {
        throw new Error('يجب أن يكون كلا الوسيطتين أرقاما.');
    }
    if (b === 0) {
        throw new Error('لا يمكن القسمة على صفر.');
    }
    return a / b;
}

try {
    console.log(divide(10, 2));  // 5
    console.log(divide(10, 0));  // يرمي خطأ!
} catch (error) {
    console.log('خطأ:', error.message);
    // "لا يمكن القسمة على صفر."
}

// التحقق من وسيطات الدالة
function createUser(name, email) {
    if (!name || typeof name !== 'string') {
        throw new Error('الاسم مطلوب ويجب أن يكون نصا.');
    }
    if (!email || !email.includes('@')) {
        throw new Error('عنوان بريد إلكتروني صالح مطلوب.');
    }
    return { name, email, createdAt: new Date() };
}

try {
    const user = createUser('', 'invalid');
} catch (error) {
    console.log(error.message);
    // "الاسم مطلوب ويجب أن يكون نصا."
}

try {
    const user = createUser('Alice', 'not-an-email');
} catch (error) {
    console.log(error.message);
    // "عنوان بريد إلكتروني صالح مطلوب."
}
تحذير: يمكنك تقنيا رمي أي قيمة في JavaScript -- نصوص أو أرقام أو كائنات -- وليس فقط كائنات Error. لكن يجب دائما رمي كائنات Error لأنها تتضمن خصائص name وmessage وstack الضرورية للتصحيح. رمي نص عادي مثل throw 'فشل شيء ما' يفقد تتبع المكدس ويجعل الأخطاء أصعب بكثير في التتبع.

أنواع الأخطاء المدمجة

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

مثال: أنواع أخطاء JavaScript

// TypeError -- استخدام نوع خاطئ في عملية
try {
    null.toString();
} catch (e) {
    console.log(e.name); // "TypeError"
    console.log(e.message); // "Cannot read properties of null"
}

try {
    const num = 42;
    num(); // محاولة استدعاء رقم كدالة
} catch (e) {
    console.log(e.name); // "TypeError"
}

// ReferenceError -- الوصول إلى متغير غير معلن
try {
    console.log(nonExistentVariable);
} catch (e) {
    console.log(e.name); // "ReferenceError"
    console.log(e.message); // "nonExistentVariable is not defined"
}

// RangeError -- القيمة خارج النطاق المسموح
try {
    const arr = new Array(-1); // طول مصفوفة سالب
} catch (e) {
    console.log(e.name); // "RangeError"
    console.log(e.message); // "Invalid array length"
}

try {
    const num = 1;
    num.toFixed(200); // الحد الأقصى هو 100
} catch (e) {
    console.log(e.name); // "RangeError"
}

// SyntaxError -- يُرمى بواسطة JSON.parse و eval مع بناء جملة غير صالح
try {
    JSON.parse('{invalid}');
} catch (e) {
    console.log(e.name); // "SyntaxError"
}

// URIError -- ترميز/فك ترميز URI غير صالح
try {
    decodeURIComponent('%');
} catch (e) {
    console.log(e.name); // "URIError"
    console.log(e.message); // "URI malformed"
}

معالجة أنواع أخطاء محددة

يمكنك استخدام instanceof للتحقق من نوع الخطأ ومعالجة أخطاء مختلفة بشكل مختلف داخل كتلة catch واحدة.

مثال: معالجة الأخطاء حسب النوع

function processInput(input) {
    try {
        const data = JSON.parse(input);
        const result = data.items.map(item => item.name.toUpperCase());
        return result;
    } catch (error) {
        if (error instanceof SyntaxError) {
            console.log('تنسيق JSON غير صالح. يرجى التحقق من مدخلاتك.');
        } else if (error instanceof TypeError) {
            console.log('بنية البيانات غير صالحة. متوقع مصفوفة items مع خصائص name.');
        } else if (error instanceof RangeError) {
            console.log('قيمة خارج النطاق المتوقع.');
        } else {
            console.log('حدث خطأ غير متوقع:', error.message);
        }
        return [];
    }
}

// مسار SyntaxError
processInput('not json');
// "تنسيق JSON غير صالح. يرجى التحقق من مدخلاتك."

// مسار TypeError (خاصية items مفقودة)
processInput('{"data": "hello"}');
// "بنية البيانات غير صالحة. متوقع مصفوفة items مع خصائص name."

// مسار النجاح
processInput('{"items": [{"name": "apple"}, {"name": "banana"}]}');
// تعيد: ["APPLE", "BANANA"]

إنشاء فئات أخطاء مخصصة

للتطبيقات الأكبر غالبا لا تكون أنواع الأخطاء المدمجة محددة بما يكفي. يمكنك إنشاء فئات أخطاء مخصصة تمتد من فئة Error لتمثيل حالات فشل محددة في تطبيقك.

مثال: فئات أخطاء مخصصة

// خطأ مخصص أساسي
class AppError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.name = 'AppError';
        this.statusCode = statusCode;
    }
}

// أنواع أخطاء محددة
class ValidationError extends AppError {
    constructor(message, field) {
        super(message, 400);
        this.name = 'ValidationError';
        this.field = field;
    }
}

class NotFoundError extends AppError {
    constructor(resource, id) {
        super(`${resource} بالمعرف ${id} غير موجود.`, 404);
        this.name = 'NotFoundError';
        this.resource = resource;
        this.resourceId = id;
    }
}

class AuthenticationError extends AppError {
    constructor(message = 'المصادقة مطلوبة.') {
        super(message, 401);
        this.name = 'AuthenticationError';
    }
}

// استخدام الأخطاء المخصصة
function getUser(id) {
    if (typeof id !== 'number' || id < 1) {
        throw new ValidationError('معرف المستخدم يجب أن يكون رقما موجبا.', 'id');
    }

    const users = { 1: 'أحمد', 2: 'سارة' };
    if (!users[id]) {
        throw new NotFoundError('المستخدم', id);
    }

    return { id, name: users[id] };
}

try {
    const user = getUser(5);
} catch (error) {
    if (error instanceof NotFoundError) {
        console.log(`404: ${error.message}`);
        console.log(`المورد: ${error.resource}، المعرف: ${error.resourceId}`);
    } else if (error instanceof ValidationError) {
        console.log(`فشل التحقق في الحقل: ${error.field}`);
        console.log(error.message);
    } else if (error instanceof AuthenticationError) {
        console.log('يرجى تسجيل الدخول للمتابعة.');
    } else {
        console.log('خطأ غير متوقع:', error.message);
    }
}
// "404: المستخدم بالمعرف 5 غير موجود."
// "المورد: المستخدم، المعرف: 5"

أنماط معالجة الأخطاء

معالجة الأخطاء الفعالة تتبع أنماطا راسخة تجعل كودك أكثر قابلية للصيانة والتنبؤ. إليك أهم الأنماط التي يجب معرفتها.

كتل try...catch المتداخلة

يمكنك تداخل كتل try...catch عندما تحتاج أقسام مختلفة من الكود إلى استراتيجيات معالجة أخطاء مختلفة.

مثال: try...catch المتداخلة

function processUserData(jsonString) {
    let rawData;

    // try الخارجية: معالجة أخطاء تحليل JSON
    try {
        rawData = JSON.parse(jsonString);
    } catch (error) {
        console.log('فشل تحليل مدخلات JSON.');
        return null;
    }

    // try الداخلية: معالجة أخطاء معالجة البيانات
    try {
        const user = {
            name: rawData.name.trim(),
            email: rawData.email.toLowerCase(),
            age: parseInt(rawData.age, 10)
        };

        if (isNaN(user.age)) {
            throw new Error('العمر يجب أن يكون رقما صالحا.');
        }

        return user;
    } catch (error) {
        console.log('فشل معالجة بيانات المستخدم:', error.message);
        return null;
    }
}

// اختبار سيناريوهات فشل مختلفة
processUserData('not json');
// "فشل تحليل مدخلات JSON."

processUserData('{"name": "Alice", "email": "alice@test.com", "age": "abc"}');
// "فشل معالجة بيانات المستخدم: العمر يجب أن يكون رقما صالحا."

processUserData('{"name": "Alice", "email": "alice@test.com", "age": "25"}');
// تعيد: {name: "Alice", email: "alice@test.com", age: 25}

إعادة رمي الأخطاء

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

مثال: إعادة رمي الأخطاء

function parseConfig(configString) {
    try {
        const config = JSON.parse(configString);

        // نريد فقط معالجة أخطاء التحقق هنا
        if (!config.apiKey) {
            throw new ValidationError('مفتاح API مطلوب.', 'apiKey');
        }
        if (!config.baseUrl) {
            throw new ValidationError('عنوان URL الأساسي مطلوب.', 'baseUrl');
        }

        return config;
    } catch (error) {
        // معالجة ValidationErrors فقط في هذا المستوى
        if (error instanceof ValidationError) {
            console.log(`فشل التحقق من الإعدادات: ${error.message}`);
            throw error; // إعادة الرمي ليتعامل معه المستدعي
        }

        // لجميع الأخطاء الأخرى (مثل SyntaxError)، غلف وأعد الرمي
        throw new Error(`فشل تحليل الإعدادات: ${error.message}`);
    }
}

// المستدعي يتعامل مع الخطأ المعاد رميه
try {
    const config = parseConfig('{"apiKey": ""}');
} catch (error) {
    if (error instanceof ValidationError) {
        console.log('يرجى إصلاح ملف الإعدادات.');
    } else {
        console.log('خطأ في الإعدادات:', error.message);
    }
}

التدهور الأنيق

التدهور الأنيق يعني أن تطبيقك يستمر في العمل -- حتى لو بسعة مخفضة -- عند حدوث أخطاء. بدلا من الانهيار توفر سلوكا بديلا أو قيما افتراضية أو رسائل خطأ سهلة الاستخدام.

مثال: أنماط التدهور الأنيق

// النمط 1: قيم افتراضية عند الفشل
function loadUserPreferences(userId) {
    const defaults = {
        theme: 'light',
        language: 'ar',
        fontSize: 16,
        notifications: true
    };

    try {
        const stored = localStorage.getItem(`prefs_${userId}`);
        if (!stored) return defaults;

        const parsed = JSON.parse(stored);
        // دمج مع الافتراضيات لملء أي خصائص مفقودة
        return { ...defaults, ...parsed };
    } catch (error) {
        console.warn('فشل تحميل التفضيلات، استخدام الافتراضيات.');
        return defaults;
    }
}

// النمط 2: اكتشاف الميزات مع بديل
function getFormattedDate(date) {
    try {
        // جرب واجهة Intl الحديثة
        return new Intl.DateTimeFormat('ar-EG', {
            weekday: 'long',
            year: 'numeric',
            month: 'long',
            day: 'numeric'
        }).format(date);
    } catch (error) {
        // بديل للتنسيق الأساسي
        return date.toDateString();
    }
}

// النمط 3: منطق إعادة المحاولة
function fetchWithRetry(url, maxRetries = 3) {
    let lastError;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            console.log(`المحاولة ${attempt} من ${maxRetries}...`);
            // محاكاة عملية جلب
            const response = makeRequest(url);
            return response; // نجاح، أعد فورا
        } catch (error) {
            lastError = error;
            console.warn(`فشلت المحاولة ${attempt}: ${error.message}`);

            if (attempt < maxRetries) {
                console.log('إعادة المحاولة...');
            }
        }
    }

    // فشلت جميع المحاولات
    throw new Error(`فشل بعد ${maxRetries} محاولات: ${lastError.message}`);
}

التحقق من المدخلات مع الأخطاء

أحد أكثر استخدامات معالجة الأخطاء شيوعا هو التحقق من مدخلات المستخدم. برمي أخطاء وصفية أثناء التحقق يمكنك توفير ملاحظات واضحة حول ما حدث خطأ وكيفية إصلاحه.

مثال: التحقق الشامل من المدخلات

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

function validateRegistrationForm(formData) {
    const errors = [];

    // التحقق من الاسم
    if (!formData.name || formData.name.trim().length === 0) {
        errors.push(new ValidationError(
            'الاسم مطلوب.', 'name', formData.name
        ));
    } else if (formData.name.trim().length < 2) {
        errors.push(new ValidationError(
            'يجب أن يكون الاسم حرفين على الأقل.', 'name', formData.name
        ));
    }

    // التحقق من البريد الإلكتروني
    const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!formData.email) {
        errors.push(new ValidationError(
            'البريد الإلكتروني مطلوب.', 'email', formData.email
        ));
    } else if (!emailPattern.test(formData.email)) {
        errors.push(new ValidationError(
            'يرجى إدخال عنوان بريد إلكتروني صالح.', 'email', formData.email
        ));
    }

    // التحقق من كلمة المرور
    if (!formData.password) {
        errors.push(new ValidationError(
            'كلمة المرور مطلوبة.', 'password', ''
        ));
    } else if (formData.password.length < 8) {
        errors.push(new ValidationError(
            'يجب أن تكون كلمة المرور 8 أحرف على الأقل.', 'password', ''
        ));
    }

    // التحقق من العمر
    const age = Number(formData.age);
    if (isNaN(age) || age < 13 || age > 120) {
        errors.push(new ValidationError(
            'يجب أن يكون العمر رقما بين 13 و 120.', 'age', formData.age
        ));
    }

    if (errors.length > 0) {
        return { valid: false, errors };
    }

    return { valid: true, errors: [] };
}

// الاستخدام
const result = validateRegistrationForm({
    name: 'أ',
    email: 'not-an-email',
    password: '123',
    age: 'عشرة'
});

if (!result.valid) {
    result.errors.forEach(err => {
        console.log(`${err.field}: ${err.message}`);
    });
}
// "name: يجب أن يكون الاسم حرفين على الأقل."
// "email: يرجى إدخال عنوان بريد إلكتروني صالح."
// "password: يجب أن تكون كلمة المرور 8 أحرف على الأقل."
// "age: يجب أن يكون العمر رقما بين 13 و 120."

أمثلة واقعية لمعالجة الأخطاء

لنلقِ نظرة على سيناريوهات عملية ستواجهها كمطور وكيفية معالجة الأخطاء في كل حالة.

معالجة أخطاء واجهة برمجة التطبيقات

مثال: التعامل مع استجابات API

class APIError extends Error {
    constructor(message, status, endpoint) {
        super(message);
        this.name = 'APIError';
        this.status = status;
        this.endpoint = endpoint;
    }
}

async function fetchData(endpoint) {
    try {
        const response = await fetch(endpoint);

        if (!response.ok) {
            // استجابات خطأ HTTP (4xx، 5xx) لا ترمي تلقائيا
            if (response.status === 404) {
                throw new APIError(
                    'المورد المطلوب غير موجود.',
                    404, endpoint
                );
            } else if (response.status === 401) {
                throw new APIError(
                    'يجب تسجيل الدخول للوصول إلى هذا المورد.',
                    401, endpoint
                );
            } else if (response.status === 403) {
                throw new APIError(
                    'ليس لديك صلاحية الوصول إلى هذا المورد.',
                    403, endpoint
                );
            } else if (response.status >= 500) {
                throw new APIError(
                    'واجه الخادم خطأ. يرجى المحاولة لاحقا.',
                    response.status, endpoint
                );
            } else {
                throw new APIError(
                    `فشل الطلب بالحالة ${response.status}.`,
                    response.status, endpoint
                );
            }
        }

        // تحليل جسم الاستجابة
        try {
            const data = await response.json();
            return data;
        } catch (parseError) {
            throw new APIError(
                'أعاد الخادم استجابة غير صالحة.',
                response.status, endpoint
            );
        }

    } catch (error) {
        if (error instanceof APIError) {
            throw error; // إعادة رمي أخطائنا المخصصة
        }

        // أخطاء الشبكة (لا إنترنت، فشل DNS، CORS، إلخ)
        if (error.name === 'TypeError' && error.message === 'Failed to fetch') {
            throw new APIError(
                'خطأ في الشبكة. يرجى التحقق من اتصالك بالإنترنت.',
                0, endpoint
            );
        }

        throw error; // إعادة رمي الأخطاء المجهولة
    }
}

// استخدام الدالة
async function loadUserProfile(userId) {
    try {
        const user = await fetchData(`/api/users/${userId}`);
        displayProfile(user);
    } catch (error) {
        if (error instanceof APIError) {
            switch (error.status) {
                case 404:
                    showMessage('المستخدم غير موجود.');
                    break;
                case 401:
                    redirectToLogin();
                    break;
                case 0:
                    showMessage('لا يوجد اتصال بالإنترنت.');
                    break;
                default:
                    showMessage(error.message);
            }
        } else {
            showMessage('حدث خطأ غير متوقع.');
            console.error(error);
        }
    }
}

معالجة النماذج

مثال: معالجة آمنة للنماذج

function processContactForm(formElement) {
    const errors = [];
    const result = {};

    try {
        // استخراج بيانات النموذج والتحقق منها
        const formData = new FormData(formElement);

        const name = formData.get('name');
        const email = formData.get('email');
        const message = formData.get('message');

        // التحقق من كل حقل
        if (!name || name.trim().length < 2) {
            errors.push('يرجى إدخال اسمك الكامل (حرفان على الأقل).');
        } else {
            result.name = name.trim();
        }

        if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
            errors.push('يرجى إدخال عنوان بريد إلكتروني صالح.');
        } else {
            result.email = email.trim().toLowerCase();
        }

        if (!message || message.trim().length < 10) {
            errors.push('يجب أن تكون الرسالة 10 أحرف على الأقل.');
        } else {
            result.message = message.trim();
        }

        if (errors.length > 0) {
            return { success: false, errors };
        }

        // معالجة البيانات المتحقق منها
        return { success: true, data: result };

    } catch (error) {
        console.error('خطأ في معالجة النموذج:', error);
        return {
            success: false,
            errors: ['حدث خطأ غير متوقع. يرجى المحاولة مرة أخرى.']
        };
    }
}

// عرض الأخطاء للمستخدم
function handleFormSubmit(event) {
    event.preventDefault();

    const result = processContactForm(event.target);

    if (result.success) {
        console.log('بيانات النموذج:', result.data);
        // إرسال إلى الخادم...
    } else {
        result.errors.forEach(error => {
            console.log('خطأ في التحقق:', error);
        });
    }
}

معالجة البيانات

مثال: معالجة ملف بيانات

function processCSVData(csvString) {
    const results = [];
    const errors = [];

    try {
        const lines = csvString.split('\n').filter(line => line.trim());

        if (lines.length < 2) {
            throw new Error('يجب أن يحتوي CSV على صف عناوين وصف بيانات واحد على الأقل.');
        }

        const headers = lines[0].split(',').map(h => h.trim());

        // معالجة كل صف على حدة -- صف سيئ واحد لا يجب أن يوقف الباقي
        for (let i = 1; i < lines.length; i++) {
            try {
                const values = lines[i].split(',').map(v => v.trim());

                if (values.length !== headers.length) {
                    throw new Error(
                        `متوقع ${headers.length} أعمدة لكن حصلت على ${values.length}`
                    );
                }

                const row = {};
                headers.forEach((header, index) => {
                    row[header] = values[index];
                });

                results.push(row);
            } catch (rowError) {
                errors.push({
                    row: i + 1,
                    message: rowError.message,
                    rawData: lines[i]
                });
            }
        }

    } catch (error) {
        return {
            success: false,
            error: error.message,
            results: [],
            rowErrors: []
        };
    }

    return {
        success: true,
        results,
        rowErrors: errors,
        totalRows: results.length + errors.length,
        successfulRows: results.length,
        failedRows: errors.length
    };
}

// اختبار مع بيانات نموذجية
const csv = `name,email,age
أحمد,ahmed@test.com,25
سارة,sara@test.com
خالد,khaled@test.com,30`;

const report = processCSVData(csv);
console.log(`تمت المعالجة: ${report.successfulRows}/${report.totalRows}`);
console.log('النتائج:', report.results);
if (report.rowErrors.length > 0) {
    console.log('الأخطاء:');
    report.rowErrors.forEach(err => {
        console.log(`  الصف ${err.row}: ${err.message}`);
    });
}
// "تمت المعالجة: 2/3"
// الصف 3 (سارة) يفشل لأنه يحتوي على عمودين بدلا من 3

أفضل ممارسات معالجة الأخطاء

اتباع هذه الممارسات الأفضل سيجعل معالجة أخطائك أكثر فعالية وتطبيقاتك أكثر متانة.

مثال: أفضل الممارسات

// 1. كن محددا فيما تصطاده
// سيئ: يصطاد كل شيء، يخفي الأخطاء
try {
    doSomethingComplex();
} catch (error) {
    console.log('حدث خطأ ما.'); // ما الذي حدث؟
}

// جيد: تعامل مع أخطاء محددة، أعد رمي غير المتوقعة
try {
    doSomethingComplex();
} catch (error) {
    if (error instanceof ValidationError) {
        showValidationMessage(error.message);
    } else if (error instanceof NetworkError) {
        showOfflineMessage();
    } else {
        throw error; // لا تبتلع الأخطاء غير المتوقعة
    }
}

// 2. لا تستخدم try/catch للتحكم في التدفق
// سيئ: استخدام الاستثناءات لتدفق البرنامج العادي
function findUser(id) {
    try {
        return users[id].name; // يرمي خطأ إذا لم يوجد المستخدم
    } catch {
        return 'غير معروف';
    }
}

// جيد: تحقق من الشروط صراحة
function findUser(id) {
    if (users[id]) {
        return users[id].name;
    }
    return 'غير معروف';
}

// 3. وفر سياقا في رسائل الخطأ
// سيئ: رسالة خطأ غامضة
throw new Error('مدخل غير صالح.');

// جيد: رسالة خطأ محددة وقابلة للتنفيذ
throw new Error(
    `تنسيق بريد إلكتروني غير صالح: "${email}". التنسيق المتوقع: user@domain.com`
);

// 4. نظف الموارد في finally
function readFile(path) {
    let fileHandle = null;
    try {
        fileHandle = openFile(path);
        return fileHandle.read();
    } catch (error) {
        throw new Error(`فشل قراءة الملف "${path}": ${error.message}`);
    } finally {
        if (fileHandle) {
            fileHandle.close(); // نظف دائما
        }
    }
}

// 5. سجل الأخطاء للتصحيح، أظهر رسائل ودية للمستخدمين
try {
    const result = processOrder(orderData);
} catch (error) {
    // للمطورين (في وحدة التحكم)
    console.error('فشلت معالجة الطلب:', {
        error: error.message,
        stack: error.stack,
        orderData
    });

    // للمستخدمين (على الشاشة)
    showUserMessage('تعذرت معالجة طلبك. يرجى المحاولة مرة أخرى.');
}
ملاحظة: معالجة الأخطاء تضيف عبئا على كودك لأن محرك JavaScript يجب أن يهيئ البنية التحتية لاصطياد الاستثناءات. تجنب لف الكود الحساس للأداء في كتل try...catch غير ضرورية. بدلا من ذلك تحقق من المدخلات مسبقا واستخدم try...catch فقط حول العمليات التي يمكن أن تفشل فعلا -- مثل تحليل البيانات الخارجية وطلبات الشبكة أو الوصول إلى نظام الملفات.
نصيحة احترافية: في تطبيقات الإنتاج فكر في استخدام معالج أخطاء مركزي يصطاد جميع الأخطاء غير المعالجة. في المتصفح يمكنك الاستماع لأحداث window.onerror وwindow.onunhandledrejection. في Node.js استخدم process.on('uncaughtException') وprocess.on('unhandledRejection'). هذه المعالجات العامة تعمل كشبكة أمان للأخطاء التي تنزلق من معالجة الأخطاء العادية.
خطأ شائع: ابتلاع الأخطاء بصمت بكتابة كتل catch فارغة مثل catch (error) {}. هذا أحد أسوأ الأنماط في JavaScript لأنه يخفي الأخطاء ويجعلها شبه مستحيلة التشخيص. إذا كنت تتوقع وتريد تجاهل خطأ فعلا أضف تعليقا يوضح السبب. وإلا سجل الخطأ دائما أو أعد رميه.

تمرين عملي

ابنِ نظام تسجيل مستخدمين كامل مع معالجة أخطاء متينة يتضمن التالي: (1) أنشئ تسلسل أخطاء مخصص مع فئة AppError أساسية وفئات فرعية متخصصة لـ ValidationError وDuplicateError وDatabaseError. كل فئة خطأ يجب أن تتضمن خاصية statusCode وأي سياق إضافي خاص بنوع الخطأ. (2) اكتب دالة registerUser تتحقق من جميع المدخلات (الاسم والبريد الإلكتروني وكلمة المرور والعمر) وتتحقق من عناوين البريد الإلكتروني المكررة مقابل مصفوفة قاعدة بيانات محاكاة وتعيد المستخدم المنشأ أو ترمي الخطأ المخصص المناسب. (3) اكتب دالة processRegistration تستدعي registerUser داخل كتلة try/catch/finally وتتعامل مع كل نوع خطأ بشكل مختلف (عرض أخطاء التحقق بشكل مضمن وإظهار رسالة تعارض للمكررات وإظهار رسالة إعادة محاولة لأخطاء قاعدة البيانات) وتسجل المحاولة دائما في كتلة finally. (4) اختبر نظامك بخمسة سيناريوهات مختلفة على الأقل: تسجيل ناجح وحقل مطلوب مفقود وبريد إلكتروني غير صالح وبريد إلكتروني مكرر وكلمة مرور قصيرة جدا. تحقق من أن كل سيناريو ينتج نوع الخطأ والرسالة الصحيحين.