JavaScript المتقدم (ES6+)

معالجة الأخطاء (Error Handling)

13 دقيقة الدرس 35 من 40

معالجة الأخطاء (Error Handling)

مرحباً بك في معالجة الأخطاء في JavaScript! في هذا الدرس، سنستكشف كيفية معالجة الأخطاء بأناقة، وإنشاء أخطاء مخصصة، وتنفيذ استراتيجيات معالجة أخطاء قوية. معالجة الأخطاء الصحيحة أمر بالغ الأهمية لبناء تطبيقات موثوقة.

فهم الأخطاء

أخطاء JavaScript هي كائنات تحتوي على معلومات حول ما حدث من خطأ في الكود الخاص بك:

مفهوم أساسي: الأخطاء توقف تنفيذ الكود ما لم يتم اصطيادها ومعالجتها. معالجة الأخطاء الجيدة تمنع الانهيارات، وتوفر ملاحظات مفيدة، وتساعد في التصحيح.

Try...Catch...Finally

عبارة try...catch...finally تسمح لك بمعالجة الأخطاء بأناقة:

Try...Catch الأساسي: try { // كود قد يطرح خطأ const result = riskyOperation(); console.log(result); } catch (error) { // معالجة الخطأ console.error('An error occurred:', error.message); } console.log('Program continues...'); مع كتلة Finally: try { const file = openFile('data.txt'); processFile(file); } catch (error) { console.error('Error processing file:', error); } finally { // تُنفذ دائماً، بغض النظر عن الخطأ closeFile(file); console.log('Cleanup complete'); }
أفضل الممارسات: استخدم كتل finally لعمليات التنظيف التي يجب أن تعمل بغض النظر عن حدوث خطأ - إغلاق الملفات، أو تحرير الموارد، أو إعادة تعيين الحالات.

أنواع الأخطاء

لدى JavaScript عدة أنواع من الأخطاء المدمجة لمواقف مختلفة:

أنواع الأخطاء المدمجة: 1. Error: خطأ عام const error = new Error('Something went wrong'); 2. SyntaxError: بناء جملة غير صالح eval('invalid code{'); // SyntaxError 3. ReferenceError: مرجع غير صالح console.log(nonExistentVariable); // ReferenceError 4. TypeError: نوع خاطئ null.toString(); // TypeError 5. RangeError: قيمة خارج النطاق new Array(-1); // RangeError 6. URIError: معالجة URI غير صالحة decodeURIComponent('%'); // URIError

رمي الأخطاء (Throwing Errors)

استخدم الكلمة المفتاحية throw لإنشاء ورمي الأخطاء:

رمي الأخطاء المدمجة: function divide(a, b) { if (b === 0) { throw new Error('Division by zero is not allowed'); } return a / b; } try { const result = divide(10, 0); console.log(result); } catch (error) { console.error(error.message); // Division by zero is not allowed } رمي أخطاء محددة النوع: function setAge(age) { if (typeof age !== 'number') { throw new TypeError('Age must be a number'); } if (age < 0 || age > 150) { throw new RangeError('Age must be between 0 and 150'); } return age; } try { setAge('twenty'); // TypeError } catch (error) { console.error(`${error.name}: ${error.message}`); }

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

أنشئ فئات أخطاء مخصصة لسيناريوهات أخطاء محددة في تطبيقك:

إنشاء أخطاء مخصصة: class ValidationError extends Error { constructor(message) { super(message); this.name = 'ValidationError'; } } class DatabaseError extends Error { constructor(message, query) { super(message); this.name = 'DatabaseError'; this.query = query; this.timestamp = new Date(); } } class AuthenticationError extends Error { constructor(message) { super(message); this.name = 'AuthenticationError'; this.statusCode = 401; } } // الاستخدام function validateEmail(email) { const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!regex.test(email)) { throw new ValidationError(`Invalid email format: ${email}`); } return true; } try { validateEmail('invalid-email'); } catch (error) { if (error instanceof ValidationError) { console.error('Validation failed:', error.message); } else { console.error('Unexpected error:', error); } }
الميزة: الأخطاء المخصصة تسمح لك بمعالجة أنواع أخطاء مختلفة بشكل مختلف وتتضمن سياقاً إضافياً محدداً لتطبيقك.

معالجة الأخطاء في Async/Await

معالجة الأخطاء في الكود غير المتزامن تتطلب اهتماماً خاصاً:

معالجة أخطاء Async/Await: async function fetchUser(id) { try { const response = await fetch(`/api/users/${id}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const user = await response.json(); return user; } catch (error) { console.error('Failed to fetch user:', error.message); throw error; // إعادة الرمي إذا كان المُستدعي بحاجة لمعالجته } } // استخدام الدالة غير المتزامنة async function displayUser(id) { try { const user = await fetchUser(id); console.log('User:', user); } catch (error) { console.error('Error displaying user:', error); } }
معالجة أخطاء Promise: fetch('/api/data') .then(response => { if (!response.ok) { throw new Error(`HTTP error ${response.status}`); } return response.json(); }) .then(data => { console.log(data); }) .catch(error => { console.error('Fetch failed:', error); }) .finally(() => { console.log('Request completed'); });

نمط حدود الأخطاء (Error Boundaries)

نفّذ حدود الأخطاء لاصطياد ومعالجة الأخطاء في أجزاء محددة من تطبيقك:

نمط حدود الأخطاء: class ErrorBoundary { constructor(fallback) { this.fallback = fallback; } wrap(fn) { return async (...args) => { try { return await fn(...args); } catch (error) { console.error('Error caught by boundary:', error); return this.fallback(error); } }; } } // الاستخدام const boundary = new ErrorBoundary((error) => { return { error: true, message: error.message }; }); const safeFunction = boundary.wrap(async (userId) => { const response = await fetch(`/api/users/${userId}`); if (!response.ok) throw new Error('User not found'); return response.json(); }); // لن يعطل هذا التطبيق const result = await safeFunction(999); console.log(result); // { error: true, message: 'User not found' }

تتبعات مكدس الأخطاء (Stack Traces)

تتضمن كائنات الأخطاء تتبعات مكدس تساعد في التصحيح:

استخدام تتبعات المكدس: function levelThree() { throw new Error('Something went wrong'); } function levelTwo() { levelThree(); } function levelOne() { levelTwo(); } try { levelOne(); } catch (error) { console.log(error.name); // Error console.log(error.message); // Something went wrong console.log(error.stack); // تتبع المكدس الكامل // Error: Something went wrong // at levelThree (...) // at levelTwo (...) // at levelOne (...) }

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

أعد معالجات أخطاء عامة للأخطاء غير المعالجة ورفض Promise:

معالجات الأخطاء العامة: // معالجة الأخطاء غير المصطادة window.addEventListener('error', (event) => { console.error('Global error caught:', event.error); // تسجيل في خدمة تتبع الأخطاء logErrorToService(event.error); // منع معالجة أخطاء المتصفح الافتراضية event.preventDefault(); }); // معالجة رفض Promise غير المعالج window.addEventListener('unhandledrejection', (event) => { console.error('Unhandled promise rejection:', event.reason); // تسجيل في خدمة تتبع الأخطاء logErrorToService(event.reason); // منع معالجة المتصفح الافتراضية event.preventDefault(); }); // معالجات أخطاء Node.js العامة process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); // التنظيف والخروج process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled rejection at:', promise, 'reason:', reason); });
مهم: يجب استخدام معالجات الأخطاء العامة كملاذ أخير. حاول دائماً معالجة الأخطاء على المستوى المناسب في كودك حيث لديك السياق الضروري.

استراتيجيات استعادة الأخطاء

نفّذ استراتيجيات للاستعادة من الأخطاء بأناقة:

نمط إعادة المحاولة (Retry): async function fetchWithRetry(url, maxRetries = 3) { for (let i = 0; i <= maxRetries; i++) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } catch (error) { if (i === maxRetries) { throw new Error(`Failed after ${maxRetries} retries: ${error.message}`); } // الانتظار قبل إعادة المحاولة (تراجع أُسي) const delay = Math.pow(2, i) * 1000; console.log(`Retry ${i + 1} after ${delay}ms`); await new Promise(resolve => setTimeout(resolve, delay)); } } } نمط البديل (Fallback): async function getDataWithFallback() { try { return await fetchFromPrimaryAPI(); } catch (primaryError) { console.warn('Primary API failed, trying backup'); try { return await fetchFromBackupAPI(); } catch (backupError) { console.error('Both APIs failed, using cache'); return getCachedData(); } } }

البرمجة الدفاعية

اكتب كوداً دفاعياً لمنع الأخطاء قبل حدوثها:

التحقق من المدخلات: function processUser(user) { // التحقق من المدخل if (!user) { throw new TypeError('User is required'); } if (typeof user !== 'object') { throw new TypeError('User must be an object'); } if (!user.name || typeof user.name !== 'string') { throw new ValidationError('User must have a valid name'); } if (!user.email || typeof user.email !== 'string') { throw new ValidationError('User must have a valid email'); } // معالجة المستخدم بأمان return { name: user.name.trim(), email: user.email.toLowerCase().trim() }; } التسلسل الاختياري و Nullish Coalescing: function getUserCity(user) { // الوصول الآمن للخصائص const city = user?.address?.city ?? 'Unknown'; return city; } // بدون التسلسل الاختياري، سيطرح هذا خطأ إذا كان user أو address null const city = getUserCity(null); // 'Unknown' بدلاً من خطأ

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

نفّذ تسجيل أخطاء شامل لتطبيقات الإنتاج:

فئة مسجل الأخطاء: class ErrorLogger { constructor(environment = 'development') { this.environment = environment; this.logs = []; } log(error, context = {}) { const errorLog = { timestamp: new Date().toISOString(), environment: this.environment, error: { name: error.name, message: error.message, stack: error.stack }, context, userAgent: navigator.userAgent, url: window.location.href }; this.logs.push(errorLog); // تسجيل في الكونسول في التطوير if (this.environment === 'development') { console.error('Error logged:', errorLog); } // إرسال إلى خدمة تتبع الأخطاء في الإنتاج if (this.environment === 'production') { this.sendToService(errorLog); } return errorLog; } sendToService(errorLog) { // إرسال إلى خدمة تتبع الأخطاء (Sentry، LogRocket، إلخ.) fetch('/api/errors', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(errorLog) }).catch(err => { console.error('Failed to send error log:', err); }); } getLogs() { return this.logs; } clearLogs() { this.logs = []; } } // الاستخدام const logger = new ErrorLogger('production'); try { riskyOperation(); } catch (error) { logger.log(error, { userId: currentUser.id, action: 'data-processing' }); }

مثال من العالم الحقيقي: عميل API مع معالجة الأخطاء

بناء عميل API قوي مع معالجة شاملة للأخطاء:

عميل API: class APIError extends Error { constructor(message, statusCode, response) { super(message); this.name = 'APIError'; this.statusCode = statusCode; this.response = response; } } class APIClient { constructor(baseURL, options = {}) { this.baseURL = baseURL; this.timeout = options.timeout || 10000; this.retries = options.retries || 3; } async request(endpoint, options = {}) { const url = `${this.baseURL}${endpoint}`; const controller = new AbortController(); // معالجة المهلة const timeoutId = setTimeout(() => { controller.abort(); }, this.timeout); try { const response = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timeoutId); // معالجة أخطاء HTTP if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new APIError( errorData.message || `HTTP ${response.status}`, response.status, errorData ); } return await response.json(); } catch (error) { clearTimeout(timeoutId); // معالجة أنواع أخطاء مختلفة if (error.name === 'AbortError') { throw new APIError('Request timeout', 408, null); } if (error instanceof APIError) { throw error; } // أخطاء الشبكة throw new APIError( `Network error: ${error.message}`, 0, null ); } } async get(endpoint) { return this.request(endpoint, { method: 'GET' }); } async post(endpoint, data) { return this.request(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); } } // الاستخدام const api = new APIClient('https://api.example.com', { timeout: 5000, retries: 3 }); async function loadUserData(userId) { try { const user = await api.get(`/users/${userId}`); console.log('User loaded:', user); return user; } catch (error) { if (error instanceof APIError) { switch (error.statusCode) { case 404: console.error('User not found'); break; case 401: console.error('Unauthorized'); // إعادة توجيه إلى تسجيل الدخول break; case 408: console.error('Request timeout'); // إعادة المحاولة أو إظهار رسالة break; default: console.error('API error:', error.message); } } else { console.error('Unexpected error:', error); } throw error; } }

تمرين تطبيقي:

المهمة: أنشئ آلة حاسبة آمنة مع معالجة شاملة للأخطاء:

  1. فئات أخطاء مخصصة لأنواع أخطاء مختلفة
  2. التحقق من المدخلات لجميع العمليات
  3. استعادة الأخطاء بقيم افتراضية
  4. تسجيل الأخطاء

الحل:

class CalculatorError extends Error { constructor(message) { super(message); this.name = 'CalculatorError'; } } class DivisionByZeroError extends CalculatorError { constructor() { super('Division by zero is not allowed'); this.name = 'DivisionByZeroError'; } } class InvalidInputError extends CalculatorError { constructor(input) { super(`Invalid input: ${input}. Expected a number.`); this.name = 'InvalidInputError'; this.input = input; } } class SafeCalculator { constructor() { this.errors = []; } validateInput(a, b) { if (typeof a !== 'number' || isNaN(a)) { throw new InvalidInputError(a); } if (typeof b !== 'number' || isNaN(b)) { throw new InvalidInputError(b); } } logError(error) { this.errors.push({ timestamp: new Date(), error: error.message, type: error.name }); } add(a, b) { try { this.validateInput(a, b); return a + b; } catch (error) { this.logError(error); throw error; } } subtract(a, b) { try { this.validateInput(a, b); return a - b; } catch (error) { this.logError(error); throw error; } } multiply(a, b) { try { this.validateInput(a, b); return a * b; } catch (error) { this.logError(error); throw error; } } divide(a, b) { try { this.validateInput(a, b); if (b === 0) { throw new DivisionByZeroError(); } return a / b; } catch (error) { this.logError(error); throw error; } } safeOperation(operation, a, b, defaultValue = null) { try { return this[operation](a, b); } catch (error) { console.warn(`Operation failed, returning default: ${defaultValue}`); return defaultValue; } } getErrors() { return this.errors; } } // الاستخدام const calc = new SafeCalculator(); console.log(calc.add(5, 3)); // 8 console.log(calc.safeOperation('divide', 10, 0, 0)); // 0 (مع تحذير) try { calc.divide(10, 0); } catch (error) { console.error(`${error.name}: ${error.message}`); } console.log(calc.getErrors()); // مصفوفة الأخطاء المسجلة

الملخص

في هذا الدرس، تعلمت:

  • استخدام try...catch...finally لمعالجة الأخطاء بأناقة
  • لدى JavaScript أنواع أخطاء مدمجة متعددة لمواقف مختلفة
  • إنشاء فئات أخطاء مخصصة لأخطاء خاصة بالتطبيق
  • معالجة أخطاء async باستخدام try...catch في async/await و .catch() في promises
  • تنفيذ حدود الأخطاء لاحتواء الأخطاء في أقسام كود محددة
  • إعداد معالجات أخطاء عامة كشبكة أمان
  • استخدام أنماط إعادة المحاولة والبديل لاستعادة الأخطاء
  • تنفيذ تسجيل أخطاء شامل لتطبيقات الإنتاج
تهانينا! لقد أكملت الوحدة 6: الوحدات وتنظيم الكود. لديك الآن فهم قوي لوحدات ES6 والمحزّمات وأنماط التصميم ومعالجة الأخطاء - مهارات أساسية لتطوير JavaScript الاحترافي!