معالجة الأخطاء وتصحيحها في TypeScript
عندما تبدأ في كتابة التطبيقات باستخدام TypeScript، تشعر سريعًا أنك دخلت إلى عالم أكثر تنظيمًا من JavaScript، عالم يفرض عليك انضباطًا جميلًا ويمنحك أدوات قوية تجعل الكود أوضح وأكثر أمانًا. لكن هذا لا يعني أن الأخطاء ستختفي. بالعكس، الأخطاء ستبقى جزءًا طبيعيًا من أي مشروع حقيقي، سواء كانت أخطاء في البيانات، أو في المنطق، أو في الاتصال بالخدمات الخارجية، أو حتى أخطاء متعلقة بالأنواع نفسها. الفرق الحقيقي أن TypeScript يساعدك على اكتشاف كثير من هذه المشاكل قبل أن تتحول إلى أعطال في وقت التشغيل.
ومع ذلك، هناك فرق كبير بين مجرد “التعامل مع الخطأ” وبين “بناء نظام متكامل لمعالجة الأخطاء وتصحيحها”. الأولى قد تعني وضع try/catch هنا وهناك بشكل عشوائي، أما الثانية فتني تنظيمًا واضحًا يفهمه المشروع كله، بحيث تعرف متى ترمي الخطأ، ومتى تعالجه، وكيف تعرض رسالة مناسبة للمستخدم، وكيف تسجل التفاصيل داخليًا، وكيف تمنع التكرار والفوضى.
في هذا المقال سنأخذ موضوع معالجة الأخطاء وتصحيحها في TypeScript من البداية إلى النهاية. سنفهم أنواع الأخطاء الشائعة، وكيفية التعامل مع الأخطاء المتزامنة وغير المتزامنة، وكيف تبني أخطاء مخصصة، وكيف تستخدم التحقق من الأنواع لتقليل المشكلات، وكيف تصمم طبقة واضحة للتعامل مع الأخطاء في تطبيقات حقيقية. وسنكتب أمثلة عملية كثيرة، لأن التعامل مع الأخطاء لا يُفهم جيدًا من الشرح النظري وحده، بل من رؤية الكود وهو يمر في حالات الفشل والنجاح معًا.
لماذا تعتبر معالجة الأخطاء مهمة جدًا في TypeScript؟
قد يظن البعض أن TypeScript يحل مشكلة الأخطاء بالكامل لأنه يكتشف الأنواع قبل التشغيل. لكن الحقيقة مختلفة قليلًا. TypeScript ممتاز في منع كثير من الأخطاء، لكنه لا يستطيع حماية تطبيقك من كل شيء. لا يستطيع مثلًا ضمان أن الـ API الخارجي سيستجيب دائمًا، أو أن قاعدة البيانات لن تتعطل، أو أن المستخدم لن يرسل قيمة فارغة أو غير متوقعة، أو أن ملفًا لم يعد موجودًا في المسار الصحيح.
هنا تظهر أهمية معالجة الأخطاء بشكل سليم. التطبيق الجيد ليس ذلك التطبيق الذي لا ينهار أبدًا، بل هو التطبيق الذي يعرف كيف يفشل بشكل أنيق. عندما يحدث خطأ، يجب أن يبقى التطبيق قابلًا للفهم، وأن يقدم رسالة واضحة، وأن يحتفظ بسجل داخلي يمكن الرجوع إليه، وأن يمنع الخطأ من التوسع إلى أجزاء أخرى من النظام.
TypeScript يساعدك في هذا لأنك تستطيع أن تجعل الأخطاء نفسها “مفصّلة” ومفهومة أكثر من خلال الأنواع. تستطيع أن تميز بين خطأ التحقق من المدخلات، وخطأ الشبكة، وخطأ الصلاحيات، وخطأ قاعدة البيانات. هذا التنظيم يغير طريقة تفكيرك في المشروع بالكامل، ويجعل الكود أكثر احترافية.
طبيعة الأخطاء في TypeScript
الأخطاء في TypeScript يمكن النظر إليها من عدة زوايا، وهذا التصنيف مهم جدًا لأنه يساعدك على اختيار طريقة المعالجة المناسبة.
أخطاء وقت الترجمة
هذه الأخطاء يكتشفها TypeScript قبل تشغيل البرنامج. مثلًا:
let age: number = "25";
هنا TypeScript سيعترض لأنك حاولت إسناد نص إلى متغير رقمي. هذا النوع من الأخطاء ممتاز لأنه يمنعك من الوصول إلى مرحلة التشغيل أصلًا.
أخطاء وقت التشغيل
وهي الأخطاء التي تحدث أثناء تنفيذ البرنامج، مثل:
محاولة قراءة خاصية من
nullفشل الاتصال بقاعدة البيانات
رفض الشبكة للطلب
استدعاء دالة على قيمة غير موجودة
تنسيق بيانات غير متوقع
مثال:
const user: { name: string } | null = null;
console.log(user.name);
هذا الكود قد يمر في بعض الحالات النظرية إذا لم يكن strict mode مضبوطًا، لكنه عند التشغيل سيسقط لأن user يساوي null.
أخطاء منطقية
وهذه من أكثر الأخطاء خداعًا. الكود يعمل، ولا يوجد crash، لكن النتيجة خاطئة. مثلًا، حساب الخصم بطريقة غير صحيحة، أو السماح بتخطي خطوة في workflow مهم، أو عرض رسالة نجاح بينما الحفظ لم يتم فعليًا.
النوع الثالث هو الأصعب غالبًا، لأنه لا يصرخ في وجهك مباشرة، بل يتسلل بهدوء. ولهذا تحتاج إلى اختبارات قوية، وقيود جيدة، وتنظيم واضح للمنطق.
بداية صحيحة: تفعيل Strict Mode
أحد أفضل القرارات التي يمكنك اتخاذها في TypeScript هو تفعيل strict داخل tsconfig.json. هذا يجعل المحول أكثر صرامة ويجبرك على التعامل مع المشكلات مبكرًا بدل أن تكتشفها في الإنتاج.
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"esModuleInterop": true
}
}
عندما يكون strict مفعّلًا، فإن TypeScript سيمنع كثيرًا من الحالات التي قد تتسبب في أخطاء لاحقًا. مثل التعامل مع undefined وكأنه قيمة مؤكدة، أو استخدام متغير بلا نوع واضح، أو تجاهل احتمالات null.
هذا ليس مجرد إعداد تقني، بل هو أسلوب تفكير. عندما تعمل في strict mode، فإنك تبدأ في كتابة كود يحترم الواقع بدل الكود الذي يفترض أن كل شيء سيمر بسلاسة. والواقع نادرًا ما يكون سلسًا.
الفكرة الأساسية في التعامل مع الأخطاء
قبل أن نكتب كودًا، من المهم أن نفهم فلسفة التعامل مع الخطأ. هناك ثلاث خطوات رئيسية:
اكتشف الخطأ مبكرًا قدر الإمكان.
عالجه في المستوى المناسب.
لا تُخفِ الخطأ الحقيقي خلف رسائل غامضة.
مثلاً، إذا كان هناك خلل في بيانات المستخدم، فمن الأفضل أن تظهر رسالة واضحة تقول إن الحقل المطلوب مفقود، بدل أن تترك الخطأ يتقدم حتى يسبب انهيارًا في طبقة أخرى.
وفي المقابل، إذا كان الخطأ داخليًا جدًا ولا ينبغي كشفه للمستخدم، فهنا يجب تسجيله داخليًا وإظهار رسالة عامة مهذبة بدل التفاصيل التقنية.
هذا التوازن مهم جدًا في تطبيقات TypeScript، خاصة عندما تبني APIs أو تطبيقات كاملة فيها طبقات متعددة.
استخدام try/catch بالشكل الصحيح
أبسط طريقة للتعامل مع الخطأ هي try/catch. لكنها للأسف تُستخدم أحيانًا بطريقة سطحية.
مثال أساسي:
try {
const result = JSON.parse("{ invalid json }");
console.log(result);
} catch (error) {
console.error("حدث خطأ أثناء تحليل JSON:", error);
}
هذا المثال بسيط، لكنه يوضح المبدأ: ضع الكود الذي قد يفشل داخل try، ثم التقط الخطأ في catch.
لكن في TypeScript، هناك نقطة مهمة جدًا: نوع error داخل catch ليس دائمًا Error تلقائيًا. في الإصدارات الحديثة ومع الإعدادات الصارمة، قد يكون unknown. وهذا جيد، لأنه يجبرك على التحقق.
مثال صحيح:
try {
throw new Error("Something went wrong");
} catch (error: unknown) {
if (error instanceof Error) {
console.error(error.message);
} else {
console.error("حدث خطأ غير معروف");
}
}
التحقق من النوع هنا مهم. لا تفترض مباشرة أن error.message موجود. أحيانًا تكون القيمة المرمية نصًا، أو كائنًا، أو شيئًا غير متوقع.
لماذا يجب أن تعتبر error في catch قيمة unknown؟
السبب بسيط: في JavaScript يمكن رمي أي شيء باستخدام throw. ليس شرطًا أن يكون Error.
throw "Error happened";
throw 404;
throw { message: "bad request" };
لذلك، TypeScript يتعامل بحذر ويقول لك: لا تتعامل مع الخطأ وكأنه من نوع واحد قبل أن تتحقق. هذه الحماية جيدة جدًا لأنها تمنعك من كتابة كود يبدو صحيحًا لكنه يفشل عند حالات غير متوقعة.
في مشاريع حقيقية، من الأفضل أن تعتمد على Error بشكل أساسي داخل مشروعك، وأن تحوّل الحالات غير المتوقعة إلى خطأ منظم يمكن التعامل معه.
إنشاء أخطاء مخصصة Custom Errors
في التطبيقات الجدية، الخطأ العام Error غالبًا لا يكفي. تحتاج إلى أخطاء تعبر عن نوع المشكلة بدقة. مثل:
ValidationError
AuthenticationError
AuthorizationError
NotFoundError
DatabaseError
NetworkError
يمكنك إنشاء أخطاء مخصصة بسهولة:
class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
}
}
الاستخدام:
function validateEmail(email: string) {
if (!email.includes("@")) {
throw new ValidationError("البريد الإلكتروني غير صالح");
}
}
ثم عند المعالجة:
try {
validateEmail("invalid-email");
} catch (error: unknown) {
if (error instanceof ValidationError) {
console.log("خطأ تحقق:", error.message);
} else if (error instanceof Error) {
console.log("خطأ عام:", error.message);
} else {
console.log("خطأ غير معروف");
}
}
الفائدة هنا كبيرة جدًا. بدل أن تتعامل مع كل الأخطاء بنفس الرسالة، يمكنك أن تعرض لكل حالة استجابة مناسبة. المستخدم يفهم أكثر، وأنت تفهم المشكلة أسرع، والكود يصبح أسهل في الصيانة.
ملاحظة مهمة عند توسيع Error
عند إنشاء خطأ مخصص، من الجيد أن تضبط prototype في بعض البيئات القديمة أو عند التعامل مع transpilation معقد. في TypeScript الحديث ومع البيئات الحالية، غالبًا يكفي هذا الشكل، لكن في بعض المشاريع القديمة قد تحتاج إلى:
class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
هذا يضمن أن instanceof ValidationError يعمل كما تتوقع.
التعامل مع الأخطاء في الدوال العادية
أحيانًا يكون من الأفضل ألا تلتقط الخطأ داخل الدالة نفسها، بل أن تتركه يصعد إلى الطبقة الأعلى. ليس كل try/catch مفيدًا. وضعه في مكان غير مناسب قد يعقد الكود بلا داعٍ.
مثال:
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error("لا يمكن القسمة على صفر");
}
return a / b;
}
هنا الدالة ترمي الخطأ، والمستدعي هو من يقرر كيف يعالجه.
try {
const result = divide(10, 0);
console.log(result);
} catch (error: unknown) {
console.error("فشل الحساب:", error);
}
هذا الأسلوب أفضل من إخفاء الخطأ داخل الدالة وإرجاع قيمة غامضة مثل -1 أو 0 في كل مرة، لأن ذلك قد يسبب لبسًا أكبر لاحقًا.
متى تعالج الخطأ داخل الدالة، ومتى ترميه للأعلى؟
هذه من أهم الأسئلة في تصميم التطبيقات.
تعالج الخطأ داخل الدالة عندما:
تستطيع فعل شيء مفيد فعلاً
تريد تحويل الخطأ إلى قيمة بديلة معروفة
تحتاج لتسجيله ثم المتابعة
يكون الخطأ محصورًا في نقطة صغيرة جدًا
ارمِ الخطأ للأعلى عندما:
تحتاج الطبقة الأعلى أن تقرر ما يجب فعله
لا تملك سياقًا كافيًا لاتخاذ القرار
لا تستطيع إصلاح المشكلة فعليًا
تريد أن تحافظ على منطق نظيف وواضح
هذه ليست قاعدة جامدة، لكنها مبدأ عملي ممتاز.
التعامل مع الأخطاء في Async/Await
في التطبيقات الحديثة، كثير من الأخطاء تأتي من العمليات غير المتزامنة: طلبات HTTP، قراءة الملفات، قواعد البيانات، الرسائل، المهام الخلفية. وهنا يجب أن تكون حذرًا جدًا.
مثال:
async function fetchUser() {
const response = await fetch("https://api.example.com/user/1");
if (!response.ok) {
throw new Error(`فشل الطلب: ${response.status}`);
}
return response.json();
}
وعند الاستدعاء:
async function main() {
try {
const user = await fetchUser();
console.log(user);
} catch (error: unknown) {
if (error instanceof Error) {
console.error("حدث خطأ:", error.message);
} else {
console.error("حدث خطأ غير معروف");
}
}
}
إذا نسيت try/catch في async function، قد يتحول الخطأ إلى unhandled rejection، وهو شيء مزعج جدًا، وقد يؤدي في الإنتاج إلى مشاكل خطيرة.
مثال عملي: التعامل مع API وتفسير الأخطاء
لنفترض أنك تبني دالة تتصل بخدمة خارجية وتسترجع قائمة المقالات.
type Post = {
id: number;
title: string;
content: string;
};
async function getPosts(): Promise<Post[]> {
const response = await fetch("https://api.example.com/posts");
if (!response.ok) {
throw new Error(`فشل تحميل المقالات: ${response.status}`);
}
const data: unknown = await response.json();
if (!Array.isArray(data)) {
throw new ValidationError("تنسيق البيانات غير صحيح");
}
return data as Post[];
}
هنا استخدمنا unknown عند استلام البيانات. وهذا مهم جدًا. لا تفترض أن الـ API سيعيد لك ما تريد دائمًا. تحقق أولًا، ثم حوّل النوع بعد التأكد.
التحقق من الأنواع Type Guards
TypeScript يمنحك أدوات قوية جدًا لتحديد النوع الحقيقي داخل الكود. هذه الأدوات تساعدك في تصحيح الأخطاء قبل أن تتفاقم.
مثال:
function isError(error: unknown): error is Error {
return error instanceof Error;
}
الاستخدام:
try {
throw new Error("Oops");
} catch (error: unknown) {
if (isError(error)) {
console.log(error.message);
}
}
يمكنك أيضًا التحقق من كائنات البيانات:
type User = {
name: string;
email: string;
};
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"name" in value &&
"email" in value
);
}
هذا النوع من التحقق مهم جدًا عندما تستقبل بيانات من مصادر خارجية. فهو لا يحميك فقط من الأعطال، بل يجعل الكود أكثر وضوحًا واستقرارًا.
التمييز بين null و undefined
كثير من الأخطاء في TypeScript تأتي من تجاهل null وundefined. في JavaScript، هاتان القيمتان شائعتان جدًا، وفي TypeScript يجب التعامل معهما بدقة.
مثال:
function greet(name?: string) {
console.log(`Hello, ${name.toUpperCase()}`);
}
هذا الكود فيه مشكلة، لأن name قد يكون undefined.
الصحيح:
function greet(name?: string) {
if (!name) {
console.log("Hello, guest");
return;
}
console.log(`Hello, ${name.toUpperCase()}`);
}
أو:
function greet(name: string | undefined) {
console.log(`Hello, ${name?.toUpperCase() ?? "GUEST"}`);
}
معرفة متى تستخدم الاختيارية، ومتى تضع قيمة افتراضية، ومتى ترفض القيمة أصلًا، من أساسيات تقليل الأخطاء.
استخدام Optional Chaining و Nullish Coalescing بحذر
هذه الأدوات مفيدة جدًا، لكنها ليست عصا سحرية.
const city = user?.address?.city ?? "Unknown";
هنا نحصل على قيمة آمنة نسبيًا، لكن لا تستخدم هذا الأسلوب لإخفاء مشكلات منطقية حقيقية. أحيانًا وجود undefined يعني أن البيانات الأساسية لم تُحمّل أصلًا، وفي هذه الحالة يجب إيقاف التنفيذ أو عرض خطأ واضح بدل تمرير قيمة افتراضية خاطئة.
القاعدة الجيدة: استخدم هذه الأدوات عندما تكون القيمة البديلة منطقية فعلًا، وليس فقط لتجنب ظهور خطأ.
التصحيح Debugging في TypeScript
معالجة الأخطاء ليست فقط كتابة try/catch. أحيانًا تحتاج إلى تصحيح أخطاء فعلية داخل المشروع. وهنا تبدأ مرحلة الـ debugging.
1. استخدام console بذكاء
console.log("value:", value);
console.warn("something suspicious");
console.error("unexpected error", error);
لكن لا تترك console.log في كل مكان داخل المشروع النهائي. استخدمه لفهم المشكلة، ثم نظّف الكود.
2. استخدام debugger
function calculateTotal(price: number, tax: number) {
debugger;
return price + tax;
}
عند تشغيل المشروع في بيئة تدعم debugging، سيتوقف التنفيذ عند هذه النقطة.
3. قراءة Stack Trace
الـ stack trace مهم جدًا لأنه يخبرك أين بدأ الخطأ، وكيف وصل. لا تكتفِ بالرسالة فقط. أحيانًا الرسالة قصيرة لكن السطر الذي بدأ منه الخطأ يكشف السبب الحقيقي.
4. تتبع البيانات خطوة خطوة
عندما يكون الخطأ منطقيًا، قسّم الدالة إلى خطوات صغيرة، وتأكد من قيمة كل خطوة قبل الانتقال للخطوة التالية.
كتابة أخطاء مفهومة
رسالة الخطأ الجيدة توفر وقتًا هائلًا. بدل:
throw new Error("Failed");
اكتب:
throw new Error("فشل تسجيل المستخدم: البريد الإلكتروني مستخدم مسبقًا");
لكن انتبه لشيء مهم: لا تكشف تفاصيل حساسة للمستخدم النهائي إذا كانت الرسالة داخلية. يمكنك كتابة رسائل مفهومة داخليًا، ثم تحويلها عند العرض للمستخدم إلى صيغة بسيطة وآمنة.
مثال:
class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number
) {
super(message);
this.name = "AppError";
}
}
ثم:
throw new AppError("Duplicate email in users table", "USER_EMAIL_EXISTS", 409);
وفي طبقة العرض:
catch (error: unknown) {
if (error instanceof AppError) {
return res.status(error.statusCode).json({
message: "لا يمكن إنشاء الحساب",
code: error.code,
});
}
}
بناء طبقة مركزية لمعالجة الأخطاء
في المشاريع الكبيرة، لا تريد أن تكرر منطق الأخطاء في كل ملف. الأفضل أن تبني طبقة مركزية.
مثال مبسط:
function handleError(error: unknown) {
if (error instanceof ValidationError) {
return {
status: 400,
message: error.message,
};
}
if (error instanceof Error) {
return {
status: 500,
message: "حدث خطأ غير متوقع",
};
}
return {
status: 500,
message: "حدث خطأ غير معروف",
};
}
هذا الأسلوب مفيد جدًا عندما تعمل على API أو backend service. كل الأخطاء تمر من نقطة معالجة واحدة، مما يسهل التسجيل، والتصنيف، والردود الموحدة.
التعامل مع الأخطاء في Node.js باستخدام TypeScript
في Node.js، قد تواجه أخطاء أثناء قراءة الملفات، والاتصال بالشبكة، وتشغيل أوامر النظام.
مثال قراءة ملف:
import { readFile } from "fs/promises";
async function loadConfig() {
try {
const content = await readFile("config.json", "utf-8");
return JSON.parse(content);
} catch (error: unknown) {
if (error instanceof Error) {
throw new Error(`فشل تحميل الإعدادات: ${error.message}`);
}
throw new Error("فشل تحميل الإعدادات");
}
}
هذا مثال جيد لأنه يعكس الخطأ بطريقة مفهومة مع الحفاظ على تفاصيل مناسبة.
التعامل مع الأخطاء في Frontend
في الواجهة الأمامية، معالجة الخطأ لها بعد إضافي: تجربة المستخدم.
إذا فشل الطلب من السيرفر، لا يكفي أن تسجل الخطأ في console. يجب أن تعرض للمستخدم ما حدث بطريقة إنسانية وواضحة.
مثال:
async function loadProfile() {
try {
const response = await fetch("/api/profile");
if (!response.ok) {
throw new Error("Failed to load profile");
}
const data = await response.json();
return data;
} catch (error: unknown) {
setError(
error instanceof Error
? error.message
: "حدث خطأ أثناء تحميل الملف الشخصي"
);
}
}
في التطبيقات الأمامية، الرسالة يجب أن تكون مفهومة، والمستخدم يجب أن يعرف ما يمكنه فعله: إعادة المحاولة، تسجيل الدخول، أو التواصل مع الدعم.
لا تستخدم try/catch لإخفاء المشاكل
من الأخطاء الشائعة أن تضع try/catch حول كل شيء وتكتب داخله:
catch (error) {
// nothing
}
هذا أسلوب سيئ جدًا. لأنك بهذه الطريقة لا تعالج الخطأ، بل تتجاهله. وقد يسبب ذلك أخطاء صامتة يصعب اكتشافها لاحقًا.
الأفضل إما:
تسجيل الخطأ
إعادة رميه
تحويله إلى قيمة مفهومة
عرض رسالة واضحة
لكن لا تتجاهله.
إرجاع Result بدل الرمي في بعض الحالات
في بعض المشاريع، قد تفضل ألا ترمي الأخطاء في كل مكان، بل تعيد كائنًا يوضح النجاح أو الفشل. هذا أسلوب شائع في بعض الأنماط البرمجية.
مثال:
type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
استخدامه:
function parseAge(input: string): Result<number> {
const age = Number(input);
if (Number.isNaN(age)) {
return { success: false, error: "العمر يجب أن يكون رقمًا" };
}
return { success: true, data: age };
}
ثم:
const result = parseAge("25");
if (result.success) {
console.log(result.data);
} else {
console.error(result.error);
}
هذا الأسلوب مفيد عندما تريد جعل الأخطاء جزءًا من التدفق الطبيعي بدل استخدام الاستثناءات في كل مكان. لكنه يحتاج انضباطًا، وإلا قد يتحول إلى تعقيد آخر.
التحقق من المدخلات قبل المعالجة
كثير من الأخطاء لا يجب أن تصل أصلًا إلى منطق المشروع. يمكن منعها عند الباب.
مثال:
function createPost(title: string, content: string) {
if (!title.trim()) {
throw new ValidationError("العنوان مطلوب");
}
if (content.trim().length < 50) {
throw new ValidationError("المحتوى يجب أن يكون أطول");
}
return {
title,
content,
};
}
التحقق المبكر يوفر وقتًا ويجعل الأخطاء أكثر وضوحًا. عندما تُمنع البيانات السيئة من الدخول، يصبح الكود الداخلي أبسط بكثير.
أنواع شائعة من الأخطاء في مشاريع TypeScript
من المهم أن تتوقع الفئات الأكثر شيوعًا:
1. أخطاء null و undefined
تظهر عند افتراض وجود قيمة غير موجودة.
2. أخطاء التحويل type casting
تحدث عند تحويل نوع دون تحقق كافٍ.
3. أخطاء API
مثل عدم استجابة السيرفر أو رد غير متوقع.
4. أخطاء التحقق validation
عندما تكون البيانات المدخلة غير صالحة.
5. أخطاء المنطق business logic
عندما يكون التنفيذ صحيحًا تقنيًا لكن النتيجة خاطئة.
6. أخطاء التزامن async
مثل race conditions أو unhandled rejections.
7. أخطاء البيئات
مثل فرق الإعداد بين development وproduction.
معرفة هذه الفئات تساعدك في بناء نظام حماية أفضل.
أفضل الممارسات في معالجة الأخطاء
هناك مجموعة عادات بسيطة لكنها تصنع فرقًا كبيرًا:
استخدم
strictmodeلا تفترض أن البيانات الخارجية صحيحة
عرّف أخطاء مخصصة للأشياء المهمة
لا تُخفِ الأخطاء الصامتة
افصل بين منطق العمل ومنطق العرض
سجّل الأخطاء الداخلية
قدّم رسائل واضحة للمستخدم
تحقق من الأنواع قبل الوصول للخصائص
اكتب اختبارات لحالات الفشل، وليس النجاح فقط
لا تكرر نفس منطق الخطأ في كل مكان
هذه ليست قواعد نظرية فقط، بل نتائج مباشرة من تجارب مشاريع كثيرة تتعثر بسبب التعامل السطحي مع الأخطاء.
مثال شامل: خدمة تسجيل مستخدم
لنضع كل شيء معًا في مثال واقعي.
class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number
) {
super(message);
this.name = "AppError";
Object.setPrototypeOf(this, AppError.prototype);
}
}
class ValidationError extends AppError {
constructor(message: string) {
super(message, "VALIDATION_ERROR", 400);
this.name = "ValidationError";
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
type RegisterInput = {
name: string;
email: string;
password: string;
};
function validateRegisterInput(input: RegisterInput) {
if (!input.name.trim()) {
throw new ValidationError("الاسم مطلوب");
}
if (!input.email.includes("@")) {
throw new ValidationError("البريد الإلكتروني غير صالح");
}
if (input.password.length < 8) {
throw new ValidationError("كلمة المرور يجب أن تكون 8 أحرف على الأقل");
}
}
async function registerUser(input: RegisterInput) {
validateRegisterInput(input);
// هنا قد نتحقق من قاعدة البيانات أو نحفظ المستخدم
return {
id: 1,
...input,
};
}
async function main() {
try {
const user = await registerUser({
name: "Hassan",
email: "hassan@example.com",
password: "secret123",
});
console.log("تم التسجيل بنجاح:", user);
} catch (error: unknown) {
if (error instanceof AppError) {
console.error(`[${error.code}] ${error.message}`);
} else if (error instanceof Error) {
console.error("خطأ غير متوقع:", error.message);
} else {
console.error("خطأ غير معروف");
}
}
}
main();
هذا المثال يجمع بين:
validation
custom errors
async handling
central error handling
type-safe logic
وهذا بالضبط الاتجاه الذي يجب أن تسير فيه التطبيقات الجيدة.
التحقق والاختبار في حالات الخطأ
عندما تكتب اختبارات، لا تختبر النجاح فقط. اختبر أيضًا الفشل.
مثال:
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error("لا يمكن القسمة على صفر");
}
return a / b;
}
اختبار:
import { describe, it, expect } from "vitest";
describe("divide", () => {
it("throws when dividing by zero", () => {
expect(() => divide(10, 0)).toThrow("لا يمكن القسمة على صفر");
});
it("returns result normally", () => {
expect(divide(10, 2)).toBe(5);
});
});
الاختبارات التي تغطي الأخطاء تمنحك ثقة حقيقية، لأن المشاكل غالبًا تظهر في الحالات الطرفية، لا في الحالات المثالية.
التعامل مع الأخطاء في الطبقات المعمارية
في المشاريع الأكبر، من الأفضل أن تفكر في الأخطاء كجزء من architecture، لا كحالات فردية فقط.
مثلاً:
Data layer: قد تعالج أخطاء قاعدة البيانات
Service layer: قد تحولها إلى أخطاء منطقية مفهومة
Controller layer: قد تحولها إلى HTTP responses
UI layer: قد تعرضها للمستخدم بطريقة مناسبة
هذا الفصل يجعل المشروع أكثر نظافة وأقل تشابكًا. الخطأ لا ينتقل بشكل عشوائي بين الطبقات، بل يمر عبر تحويلات واضحة.
خطأ شائع: الاعتماد على النوع بدل القيمة
أحيانًا يكتب المطور:
const value = response.data as User;
هذا قد يكون خطيرًا إذا لم تتحقق من القيمة فعلًا. as لا يثبت صحة البيانات، بل يخبر TypeScript أن يثق بك. وإذا كنت مخطئًا، فالمشكلة ستظهر في وقت التشغيل.
الأفضل:
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"name" in value &&
"email" in value
);
}
ثم استخدام التحقق بدل الافتراض.
خطأ شائع: وضع try/catch في المكان الخطأ
ليس كل مكان يحتاج try/catch. أحيانًا تضعه حول كتلة كبيرة جدًا، فتخفي أين وقع الخطأ أصلًا. والأفضل غالبًا أن تجعل الكتل الصغيرة واضحة.
بدل:
try {
// عشرات الأسطر
} catch (error) {
// لا نعرف أين حدث الخطأ
}
فكر في تقسيم المنطق:
const parsed = parseInput(input);
const user = validateUser(parsed);
const saved = await saveUser(user);
ثم عالج الأخطاء في الحدود المناسبة. هذا أسهل في التصحيح وأكثر وضوحًا.
كيف تكتب رسائل خطأ مفيدة للمستخدم
الرسالة الجيدة ليست طويلة جدًا، وليست تقنية جدًا. يجب أن تجيب على الأسئلة الأساسية:
ماذا حدث؟
هل يستطيع المستخدم فعل شيء؟
ما الذي يجب أن يفعله الآن؟
مثال جيد:
تعذر حفظ البيانات، يرجى التحقق من الحقول وإعادة المحاولة.
مثال داخلي للمطور:
ValidationError: email format is invalid in register payload
الأول للمستخدم، والثاني للمطور أو السجل الداخلي. لا تخلط بينهما.
نصائح عملية لتصحيح الأخطاء بسرعة
عندما تواجه مشكلة في TypeScript، جرّب هذا التسلسل:
اقرأ رسالة الخطأ بالكامل.
اذهب إلى السطر الأول الذي بدأ فيه الخطأ.
تحقق من الأنواع الفعلية، لا المتوقعة فقط.
افحص
nullوundefined.تأكد من صحة البيانات القادمة من APIs.
قسّم الدالة إذا كانت طويلة جدًا.
استخدم debugger أو console في مواضع مدروسة.
اكتب اختبارًا صغيرًا يعيد إنتاج الخطأ.
هذه الطريقة تختصر ساعات من التخمين.
خلاصة عملية
معالجة الأخطاء وتصحيحها في TypeScript ليست مجرد أمر جانبي، بل هي جزء أساسي من كتابة تطبيق محترم وقابل للنمو. TypeScript يمنحك أدوات قوية جدًا لمنع الأخطاء قبل حدوثها، لكنه لا يلغي الحاجة إلى التفكير الجيد في كيفية التقاط الأخطاء وتصنيفها وتمريرها وعرضها. عندما تتعامل مع الأخطاء بوعي، يبدأ مشروعك في اكتساب شكل أكثر احترافية، وتصبح الثقة في الكود أعلى، والتصحيح أسرع، والتوسع أسهل.
الفكرة الأساسية بسيطة لكنها عميقة: لا تحاول أن تمنع كل خطأ من الظهور، بل تعلم كيف تتعامل معه بطريقة منظمة. استخدم strict mode، تحقق من القيم الخارجية، أنشئ أخطاء مخصصة عندما تحتاج، استخدم try/catch في المكان الصحيح، واكتب اختبارات تغطي الفشل مثلما تغطي النجاح. ومع الوقت ستلاحظ أن الكود لم يعد مجرد سطور تعمل، بل نظام متماسك يعرف كيف يواجه المشكلات دون أن يفقد توازنه.