1. الـ Bug اللي مفيش له صوت في الـ Logs
تعال بقي أصدمك شوية — ده مش قصة خيالية، ده حصل فعلاً.
متجر إلكتروني شغال من سنتين، قرروا يضيفوا فيتشر: استرداد المبالغ (Refunds). المطور كتب الكود في يومين، اتعمل Code Review، الـ Tests عدوا، رفعوا على الـ Production وخلاص.
الأسبوع الأول؟ كل حاجة تمام — الـ Refunds شغالة، مفيش شكاوى.
الأسبوع التاني؟ ناس بدأت تقول: “الـ Refund بيوصلني وأحياناً مش بيوصل” أو “عملت استرداد من يومين ومفيش أثر في الحساب”. يعني المشكلة مش ثابتة — أحياناً تنجح وأحياناً لا، ومن برة مفيش pattern واضح.
فريق الـ Backend راح يدور من فين المشكلة:
- فتحوا الـ Logs: مفيش Error مكتوب. لو كان في Exception أو Rejection، كان هيتسجل — فالمشكلة مش في إن السيرفر “واقع”.
- الـ Monitoring (مثلاً Prometheus، Datadog، إلخ): ولا Response 500 ولا Server Crash. الـ Request بيكمل عادي ويرد 200.
- الـ Database: راحوا يشوفوا الـ Data نفسها. لقوا إن الفلوس في الغالب بترجع فعلاً لحساب العميل — يعني الـ Payment Gateway بيشتغل. بس لما راحوا على جدول الـ Audit (اللي بيتسجل فيه كل عملية استرداد للـ Compliance والمراجعة) — لقوا إن السجل مش دايماً موجود. يعني العميل استرد فلوسه، بس من ناحية الـ System مفيش trace كامل للعملية.
فهموا إن المشكلة في حاجة بعد ما الفلوس ترجع — حاجة متعلقة بتسجيل الـ Refund في الـ Audit، ومش ظاهرة في الـ Logs ولا الـ Monitoring.
قعدوا 11 يوم بيدوروا في الكود والـ Services والـ DB. وفي الآخر؟ سطرين.
يعني من الآخر: سطرين في الكود — وده اللي هنشوفه في الـ Code Block اللي تحت.
// في refund.service.js
async function processRefund(orderId, amount) {
await paymentGateway.refund(orderId, amount); // بيرجع الفلوس ✅
auditLogger.log({ // ← مش فيه await!
action: 'REFUND',
orderId,
amount,
timestamp: new Date(),
}); // لو auditLogger.log رمى Error — محدش شايلها 💀
}
إيه اللي حصل بالظبط؟
الـ auditLogger.log دي دالة async — يعني بترجع Promise. لما بتستنى منها بالـ await، السيرفر بيقف عندها لحد ما تخلص (إما تنجح وإما ترمي Error وتلاقي حد يمسكها). لما ما تعملش await، الكود بيمشي فوراً للسطر اللي بعدها وكأنك قلت “اعمل اللي انت عايزه في الخلفية، أنا مش مستنيك”.
فلو الـ DB بتاعة الـ Audit كانت مثلاً بطيئة أو فيها ضغط، ورمت Timeout (أو أي Error تاني) — الـ Promise اللي رجعت من auditLogger.log هتترفض. بس مفيش حد واقف يستنى النتيجة ولا عامل catch على الـ Promise دي. فالـ Rejection بتتم في الصمت: مفيش Error في الـ Console، مفيش Log، الـ Request خلص ورد 200. السيرفر مكمل كأن حاجة — من ناحية الـ API، الـ Refund “نجح” (الفلوس رجعت)، ومن ناحية الـ Audit اللي فشل، محدش شايل المسؤولية فالفشل ابتُلع وماوصلش لأي Error Handler.
يعني المشكلة مش في الـ Logic. المشكلة في فهمك للـ Promise — وده اللي هنمشي عليه النهاردة.
2. الـ Promise: ليه الـ Bug مابانش؟
اللي حصل في الـ Refund كان سببه حاجة واحدة: الـ auditLogger.log بترجع Promise، ومكناش واقفين نستناها. عشان تفهم ليه ده خلى الـ Error تختفي، محتاج تعرف إيه اللي بيحصل لما الـ Promise تفشل ومحدش حاضر.
الكود العادي vs الـ Promise
في الكود العادي (سطر ورا سطر، Sync)، لو حصل غلط — السيرفر يقع في نفس اللحظة وتشوف الـ Error في الـ Console. واضح.
const user = users[undefined].name; // 💥 TypeError — فوراً
الـ Promise مش كده. الـ Promise معناها: “هعمل حاجة (زي استعلام من الـ DB) والنتيجة هتيجي بعدين”. لما النتيجة تجي، يبقى إما تمام (Fulfilled) وإما فشل (Rejected). لو فشلت ومفيش حد قال await أو عمل .catch() — الـ Error مش بتطلع قدامك. الكود اللي ناديك يمشي، والـ Response يتبعت، والـ Promise الفاشلة تفضل وحدها. ده اسمه Unhandled Promise Rejection.
مثال بنفس فكرة الـ Refund:
async function fetchUser(id) {
const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);
return user;
}
fetchUser(999); // ناديناها من برة من غير await
// لو الـ DB رمت Error → Promise اترفضت
// مفيش حد هنا يمسكها → صمت
في Node قديم كان بيطلع Warning بس السيرفر يكمل. في Node 15+ الـ Unhandled Rejection بقت تقفل السيرفر. يعني إما الـ Bug يضيع في الـ Logs (قديم) أو السيرفر يقع فجأة (جديد). عشان كده لازم أي Promise إما تـ await عليها (في try/catch) أو تعمل لها .catch().
الـ Promise من جوا: Pending → Fulfilled أو Rejected
أي Promise بتبدأ Pending (لسه الشغل جاري). بعدين إما Fulfilled (في نتيجة) وإما Rejected (في Error). الرسمة:
Pending (لسه شغالة)
│
┌────────┴────────┐
▼ ▼
Fulfilled ✅ Rejected ❌
.then(value) .catch(error)
مثال: Promise من جوا، وبعدين نستنى النتيجة بـ .then و .catch:
const userPromise = new Promise((resolve, reject) => {
db.query('SELECT * FROM users WHERE id = 1', (err, result) => {
if (err) reject(new Error('DB_CONNECTION_FAILED'));
else resolve(result[0]);
});
});
userPromise
.then(user => console.log(user.name))
.catch(err => console.log('فشل:', err.message));
شرح للـ Promise من جوا:
لما تنفذ السطر new Promise(...)، الـ function اللي جواها بتشتغل فوراً. يعني السيرفر بيبعت الاستعلام للـ DB ويمشي — مش واقف. الـ userPromise اللي عندك دي مش اليوزر نفسه؛ دي وعد إن “هجيبلك نتيجة بعدين”. النتيجة جاية منين؟ من الـ callback بتاع الـ db.query: لما الـ DB ترد (بعد ما ثانية أو خمس)، الـ callback بيتنفذ. لو فيه غلط، الكود بينادي reject(الغلط) — وده بيسيب الـ Promise في حالة Rejected. لو مفيش غلط، بينادي resolve(result[0]) — وده بيسيبها Fulfilled باليوزر. اللي عامل .then(user => ...) بياخد القيمة لما الـ Promise تبقى Fulfilled. اللي عامل .catch(err => ...) بياخد الـ Error لما تبقى Rejected. يعني الـ Promise وسيط: جواها حد بيقول “خلصت كده أو كده”، وبراها حد بيقول “استنى وامسك النتيجة أو الغلط”. لو مفيش حد براها عامل .then أو .catch (أو await)، والـ Promise اترفضت — الـ Error بتضيع. ده بالظبط اللي حصل في الـ Refund.
async/await = نفس الـ Promise، بس أبسط في القراءة
تحت الـ async/await نفس الـ Promise — مفيش حاجة جديدة. الفرق في الشكل: بدل ما تكتب سلسلة .then و .catch، تكتب await وتستنى النتيجة في متغير، ولو حصل غلط تمسكه في try/catch. أقرأ وأنضف.
async function getUser(id) {
const user = await db.findById(id);
if (!user) throw new Error('NOT_FOUND');
return user;
}
شرح للـ async والـ Rule:
الـ await معناها: “استنى الـ Promise دي تخلص. لو خلصت بنجاح، خد القيمة واكمل. لو رمت Error، ارميها برة (يعني الـ Promise اللي الـ async function راجعة بتبقى Rejected).” وخلاصة مهمة: أي async function دايماً بترجع Promise — مش القيمة اللي انت راجعها. حتى لو جواها return 5، اللي بينادي الـ function بياخد Promise لسه، والـ 5 جوا الـ Promise. عشان كده لما حد يكتب getUser(999) من برة من غير await ولا .catch()، هو بيدي لنفسه Promise ومش واقف يستناها. لو جوا الـ getUser حصل غلط (الـ DB وقعت، أو الـ user مش موجود ورمى NOT_FOUND)، الـ Promise هتترفض ومفيش حد مسكها. نفس سيناريو الـ Refund: الـ Error بتضيع، والـ Bug مابانش. عشان كده الـ Rule بتاعة البوست كله: أي async تناديها، إما تـ await عليها (وجوا try/catch عشان تمسك الـ Error) أو تعمل لها .catch(). من غير كده أنت بتسيب Promise مرفوضة من غير صاحب.
3. الكود بيكشف صاحبه: تلات مستويات في نفس المشكلة
نفس الـ Feature، نفس الـ Endpoint — بس كل مطور بيكتبه بطريقة. إزاي بتعامل مع الـ Errors والـ Async. تعال نشوف السيناريو ده بعيون تلاتة: واحد بيكتب كود “شغال” وفيه أخطاء مخفية، واحد فاهم try/catch بس لسه ناقصه حاجة، وواحد بيبني منظومة كل Error ليها معناها ورد فعل مناسب. آخرتها هنعرف إزاي نصل للشكل الصح.
السيناريو: مستخدم بعت طلب POST /orders (يعمل أوردر جديد). الـ Backend لازم يعمل أربع حاجات بالترتيب:
- يتحقق من الداتا — الـ items والـ userId موجودين وصحيحين
- يحجز المخزون — يتأكد إن المنتجات متاحة
- ينشئ الأوردر في الـ DB
- يبعت إيميل تأكيد للعميل
نفس الخطوات، بس التنفيذ مختلف من مطور للتاني.
🔴 العينة الخطيرة
الكود ده “شغال” في الـ Dev — بس فيه أربع مشاكل لو حصلت في الـ Production هتخلي السلوك غريب أو السيرفر يقع، ومش كلها واضحة من أول مرة.
// مشاكل 4 مخفية في كود "شغال":
app.post('/orders', async (req, res) => {
// ❌ مشكلة 1: لو req.body.items فاضي — Error مش متعمله handle
const items = req.body.items;
// ❌ مشكلة 2: هنا await بس مش في try/catch
// لو checkStock رمت Error → Unhandled Rejection → السيرفر يوقع
const available = await inventoryService.checkStock(items);
if (!available) {
res.json({ error: 'مش متاح' }); // ❌ مشكلة 3: مفيش return!
// الكود بيكمل تحت حتى بعد ما بعت الـ Response!
}
const order = await db.orders.create({ userId: req.user.id, items });
// ❌ مشكلة 4: ده مش فيه await — لو sendEmail فشلت: Promise اترفضت بصمت
emailService.sendConfirmation(req.user.email, order.id);
res.json({ success: true, order });
});
تفصيل المشاكل:
مشكلة 1: لو الـ Client بعت body فاضي أو من غير
items،req.body.itemsهيكونundefined. أول ما الكود يحاول يستخدمه (مثلاً فيcheckStock(items)) ممكن يرمي TypeError أو الـ Service يرمي Error. مفيش تحقق من الداتا قبل الاستخدام، ومفيش try/catch يمسك أي Error — فالـ Request هيفشل أو السيرفر يقع حسب نوع الـ Error.مشكلة 2: الـ
await inventoryService.checkStock(items)لو الـ Service رجع Error (مثلاً الـ DB وقعت)، الـ Promise هتترفض. مفيش try/catch حوالين الـ await — فـ Unhandled Rejection. في Node 15+ ده بيوقف السيرفر. يعني حاجة بعيدة (زي DB أو شبكة) تقدر تخلي الـ Process كله يقع.مشكلة 3: لما المخزون مش متاح، الكود بيعمل
res.json({ error: 'مش متاح' })— بس مش عامل return بعدها. الـ Response اتبعت، بس التنفيذ بيكمل. فالسطر اللي بعدهdb.orders.create(...)هيتنفذ برضه، وبعدينres.json({ success: true, order }). يعني السيرفر بيحاول يبعت ردين لنفس الـ Request — وده بيعطي “Cannot set headers after they are sent” أو سلوك غريب. لازم بعد أيres.jsonأوres.status(...).json(...)توقف التنفيذ بـreturn.مشكلة 4:
emailService.sendConfirmation(...)بترجع Promise. من غيرawaitولا.catch()، لو الإيميل فشل (السيرفس وقع، أو الـ SMTP رفض) الـ Promise بتترفض في الصمت — نفس قصة الـ Refund. الـ Request يخلص ويرد 200، والعميل يشوف “تم إنشاء الطلب”، بس الإيميل مش واصل ومفيش أي Error في الـ Logs.
يعني في الـ Production: أحياناً رد مزدوج أو Headers Error (مشكلة 3)، أحياناً إيميل مش بيوصل ومفيش trace (مشكلة 4)، وأحياناً السيرفر بيقع فجأة (مشكلة 2). الـ Bug مش ثابت عشان بيعتمد على توقيت الـ DB والإيميل — وده بيخلي الـ Debug أصعب.
🟡 العينة المتوسطة
النسخة دي أحسن بكتير: في تحقق من الداتا، في return بعد كل رد، والـ Email Promise عليها .catch() عشان مفيش Unhandled Rejection. كل الـ async جوا try، ولو حصل أي Error بيوصل للـ catch ويرد 500.
app.post('/orders', async (req, res) => {
try {
const { items, userId } = req.body;
if (!items || items.length === 0) {
return res.status(400).json({ error: 'الـ Items مطلوبة' }); // ✅ return
}
const available = await inventoryService.checkStock(items);
if (!available) {
return res.status(409).json({ error: 'المنتج مش متاح دلوقتي' }); // ✅
}
const order = await db.orders.create({ userId, items });
// ✅ أحسن — بس "fire and forget" مقصود هنا مع handle للخطأ
emailService.sendConfirmation(req.user.email, order.id)
.catch(err => console.error('Email failed:', err)); // ✅ catch موجود
return res.status(201).json({ success: true, order });
} catch (err) {
// 🟡 مشكلة: كل Error بتبقى 500 حتى لو كانت بتاعت المستخدم
res.status(500).json({ error: 'حصل خطأ في السيرفر' });
}
});
إيه اللي اتصلح: التحقق من الـ items، الـ return بعد كل response، والـ .catch على الإيميل. السيرفر مش هيقع من Unhandled Rejection والـ Response مش هيتبعت مرتين.
إيه اللي لسه ناقص: الـ catch واحد لكل حاجة. أي Error — سواء من الـ validation أو من الـ DB أو من الـ inventory — بتروح لنفس المكان وترد 500 ونفس الرسالة “حصل خطأ في السيرفر”. المشكلة إن مش كل الأخطاء “غلط سيرفر”. مثلاً لو الـ DB رمت UniqueConstraintError (يعني الأوردر اتعمل قبل كده — تكرار)، من المنطقي ترجع للـ Client 409 Conflict مش 500. لو الـ Client بعت داتا غلط (مثلاً productId مش موجود)، أحسن رد 400 أو 404 مش 500. يعني الـ Client والـ Monitoring مش قادرين يفرقوا بين “غلط مني” و”غلط من السيرفر” — وده بيخلي الـ Debug والـ UX أسوأ. عشان كده المستوى اللي بعده بيفرق بين أنواع الـ Errors ويرد الـ Status Code المناسب لكل نوع.
🟢 العينة المتقدمة
هنا الـ Handler نفسه مافيهوش try/catch — بس مش معناها إن مفيش Error handling. الفكرة إن الـ asyncWrapper و globalErrorHandler هم اللي بيعملوا الشغل: أي Error (مرمية من أي سطر جوا الـ Handler) بتوصل للـ Wrapper، والـ Wrapper يمررها للـ global handler. الـ global handler عنده قائمة بأنواع الـ Errors (ValidationError → 400، NotFoundError → 404، StockUnavailableError → 409، UniqueConstraintError → 409، إلخ) ويرد الـ Status Code والرسالة المناسبة. فالكود جوا الـ Handler يبقى “business logic بس” — يرمي Error من نوع معيّن والمنظومة ترد بالشكل الصح.
// الـ Handler نفسه نظيف تماماً:
app.post('/orders', asyncWrapper(async (req, res) => {
const { items, userId } = validateOrderInput(req.body); // ValidationError لو فيه مشكلة
const available = await inventoryService.checkStock(items); // NotFoundError لو مش متاح
if (!available) throw new StockUnavailableError(items);
const order = await orderService.create({ userId, items }); // DB Errors → AppError
// Fire and forget — مع tracking للفشل في الخلفية
emailService.sendConfirmation(req.user.email, order.id)
.catch(err => logger.warn('Email delivery failed', { orderId: order.id, err }));
sendResponse(res, 201, { success: true, data: order });
}));
// أي Error في أي سطر فوق ← asyncWrapper بيمسكها → globalErrorHandler بيردها بالـ Status Code الصح
فين الـ Global Handler؟ شكلهم بإختصار كده — الـ asyncWrapper بيلف الـ async handler وأي Rejection من الـ Promise بيمررها لـ next (اللي هو الـ globalErrorHandler). والـ globalErrorHandler بياخد الـ Error ويحدد الـ statusCode والرسالة حسب النوع ويرد للـ Client:
// asyncWrapper — الجسر: أي Error من الـ Handler تروح لـ next
function asyncWrapper(fn) {
return function (req, res, next) {
Promise.resolve(fn(req, res, next)).catch(next); // next = globalErrorHandler
};
}
// globalErrorHandler — يمسك كل الـ Errors ويرد بالـ Status المناسب
function globalErrorHandler(err, req, res) {
const statusCode = err.statusCode || 500; // كل Error نوع له statusCode (400, 404, 409, ...)
const message = err.message || 'حصل خطأ في السيرفر';
res.status(statusCode).json({ success: false, error: { message, code: err.code } });
}
في الـ Express عادةً بتسجّل الـ globalErrorHandler في آخر الـ middleware: app.use(globalErrorHandler). التفاصيل الكاملة (تعريف أنواع الـ AppError والـ isOperational والـ Logging) في قسم «الـ Global Error Handler» لاحقاً في البوست.
إيه اللي بيحصل في الـ Handler: الـ validateOrderInput لو لقى مشكلة بترمي ValidationError. الـ inventoryService.checkStock لو المنتج مش متاح ممكن ترمي StockUnavailableError. الـ orderService.create لو الـ DB رمت UniqueConstraintError (أوردر مكرر) أو أي DB error، الـ Service بتحولها لـ AppError من نوع معيّن. الـ Handler ما بيعملش try/catch — بيكتب الـ flow الطبيعي ويرمي Error لما حاجة غلط. الـ asyncWrapper بيمسك أي Rejection ويمررها للـ globalErrorHandler، والـ globalErrorHandler بيرد 400 أو 404 أو 409 أو 500 حسب نوع الـ Error. الإيميل برضه “fire and forget” بس عليها .catch عشان الـ failure يتسجل في الـ logger من غير ما يوقع الـ Request.
يعني: كل Error ليها نوع، كل نوع ليه Status Code. الـ Handler مافيهوش تكلف — والمنظومة كلها متسقة.
4. فخاخ الـ async/await اللي بتوقع فيها الكل
حتى لو فاهم الـ Promise والـ await، في أنماط بتتكرر وتخلي الكود يتصرف بشكل غلط — أو يبقى أبطأ من اللازم. الأربع فخاخ دي أشهر حاجات: كل واحد بيبان “طبيعي” بس النتيجة مختلفة عن اللي انت متوقعه. نمر عليهم واحد واحد ونوضح إيه اللي بيحصل وإزاي تتجنبه.
الفخ ① — await في forEach: الكود “بيشتغل” بس مش بيستنى
الفكرة: عايز تبعت إشعار لكل اليوزرز وتستنى كل الإشعارات تخلص، بعدين تطبع “تم إرسال الإشعارات”. الكود اللي تحت بيفي بالشكل ده؟ لأ.
// سؤال انترفيو كلاسيكي — إيه اللي هيظهر؟
async function notifyAllUsers(users) {
users.forEach(async (user) => { // ← forEach + async: خطأ خفي
await sendNotification(user.id);
});
console.log('✅ تم إرسال الإشعارات!'); // ← هيطلع فوراً قبل أي إشعار يتبعت!
}
إيه اللي بيحصل بالظبط: الـ forEach مش مصممة تشتغل مع الـ async. هي بتاخد function وتناديها على كل عنصر — ومن غير ما تستنى الـ function تخلص. الـ function اللي جوا الـ forEach دي async، فكل نداء بيرجع Promise. الـ forEach بتنادي الـ callback وتكمل فوراً؛ مش بتعمل await على الـ Promise. فترتيب التنفيذ: (1) forEach بتنادي callback لليوزر الأول → بتبدأ sendNotification وتمشي، (2) تنادي callback لليوزر التاني → نفس الحكاية، (3) … لحد آخر اليوزرز، (4) forEach “خلصت” (من ناحيتها — خلصت النداءات)، (5) السطر اللي بعدها console.log('✅ تم إرسال الإشعارات!') بيتنفذ فوراً. الإشعارات لسه شغالة في الخلفية. يعني الرسالة بتطلع قبل ما أي إشعار يتبعت فعلاً.
الحلول: محتاج تستنى الـ Promises. ممكن واحد ورا واحد (sequential)، أو كلهم مع بعض (parallel)، أو على دفعات (batched) عشان ما تضغطش الـ Service.
// ① Sequential — واحد ورا واحد (أبطأ، بس ترتيب الإرسال متحكم فيه):
for (const user of users) {
await sendNotification(user.id);
}
console.log('✅ تم بالترتيب!');
// ② Parallel — كل الإشعارات مع بعض (أسرع؛ كلهم يشتغلوا بنفس الوقت):
await Promise.all(users.map(user => sendNotification(user.id)));
console.log('✅ تم بالتوازي!');
// ③ Batched — مثلاً 50 في المرة (للأعداد الكبيرة؛ تقلل الضغط على الـ API):
const BATCH = 50;
for (let i = 0; i < users.length; i += BATCH) {
const batch = users.slice(i, i + BATCH);
await Promise.all(batch.map(u => sendNotification(u.id)));
if (i + BATCH < users.length) await sleep(200); // استنى شوية بين كل دفعة
}
الفخ ② — نسيان الـ return بعد res.json()
لو بعت الـ Response للـ Client ومش عامل return بعدها، الكود بيكمل. والخطوة اللي بعدها ممكن تبعت رد تاني لنفس الـ Request — وده Express (والـ HTTP) مبيسمحوش بيه: الـ Response اتبعت مرة واحدة.
app.get('/user/:id', async (req, res) => {
const user = await db.findUser(req.params.id);
if (!user) {
res.status(404).json({ error: 'مش موجود' }); // ❌ مفيش return!
// التنفيذ بيكمل تحت
}
// هنا: user = null بس الكود وصل
res.json({ data: user }); // 💥 Error: Cannot set headers after they are sent
});
إيه اللي بيحصل: لما اليوزر مش موجود، السيرفر بيعمل res.status(404).json(...) — وده بيبعت الـ Response ويسيب الـ Headers اتبعت. بعدها الـ function مابترجعش، فالكود بيكمل ويوصل لـ res.json({ data: user }). بتحاول تبعت رد تاني لنفس الـ Request. الـ Node/Express بيطلع “Cannot set headers after they are sent” لأن الـ Headers اتبعت فعلاً. حتى لو ماطلعش Error، الـ Client هياخد رد واحد غير متوقع (مثلاً 200 مع body فاضي أو غلط).
الحل: بعد أي res.status(...).json(...) أو res.json(...) لازم توقف تنفيذ الـ Handler — يعني تضع return:
if (!user) {
return res.status(404).json({ error: 'مش موجود' }); // ✅
}
res.json({ data: user }); // هيوصل بس لو user موجود
الفخ ③ — Promise.all تعني “الكل أو لا حد”
Promise.all بترجع نتيجة واحدة لما كل الـ Promises تنجح. لو واحدة فشلت، الـ Promise اللي راجعة من Promise.all بترفض فوراً — ونتائج الباقي (حتى لو خلصوا بنجاح) مش هتستخدم. عشان كده من الخطأ تستخدمها في أماكن العمليات فيها مستقلة وعايز تعرض اللي نجح حتى لو الباقي فشل.
// صفحة Dashboard — 3 مصادر بيانات منفصلة:
const [profile, orders, wishlist] = await Promise.all([
getUserProfile(userId), // ✅ نجح
getOrders(userId), // 💥 الـ DB بطيئة → Timeout بعد 5 ثواني
getWishlist(userId), // ✅ نجح في 100ms
]);
// النتيجة: الـ Promise.all كلها بترفض — فالداشبورد كلها Error 500
// رغم إن الـ Profile والـ Wishlist كانوا جاهزين
إيه اللي بيحصل: الـ ثلاث استدعاءات بيشتغلوا بالتوازي. لو getOrders رجع Error (مثلاً timeout)، الـ Promise اللي راجعة من Promise.all بتبقى Rejected. الـ Destructuring [profile, orders, wishlist] مش هيحصل — هيطلع Exception. فالسيرفر بيرد 500 واليوزر يشوف “حصل خطأ” بدل ما يشوف الـ Profile والـ Wishlist اللي نجحوا.
الحل: لما العمليات مستقلة وعايز تعرض اللي نجح وتتعامل مع اللي فشل (مثلاً تعرض “الطلبات غير متاحة دلوقتي”)، استخدم Promise.allSettled. هي بترجع مصفوفة بنتائج كل Promise — كل عنصر فيه status: 'fulfilled' و value أو status: 'rejected' و reason. تقرأ من الـ results وتاخد الـ value لو fulfilled وتستخدم fallback (مثل null أو []) لو rejected.
const results = await Promise.allSettled([
getUserProfile(userId),
getOrders(userId),
getWishlist(userId),
]);
const profile = results[0].status === 'fulfilled' ? results[0].value : null;
const orders = results[1].status === 'fulfilled' ? results[1].value : [];
const wishlist = results[2].status === 'fulfilled' ? results[2].value : [];
// اليوزر يشوف Dashboard؛ لو الـ Orders فشلت يبقى القسم ده فاضي أو رسالة "جاري التحميل"
متى تستخدم إيه:
| الدالة | بتستخدمها لما |
|---|---|
Promise.all | كل العمليات مرتبطة — لو واحدة فشلت، الباقي ميعناهوش (مثلاً: خصم فلوس + حفظ أوردر؛ لو الحفظ فشل لازم ترجع الفلوس) |
Promise.allSettled | العمليات مستقلة — عايز نتيجة كل واحد حتى لو التاني فشل (Dashboard، تجميع من مصادر كتير) |
Promise.race | عايز أول نتيجة تيجي — مثلاً Timeout: استنى أول حاجة من “النتيجة الفعلية” أو “تايم أوت” |
Promise.any | عايز أول نجاح — مثلاً تجرب 3 سيرفرات، أول رد ناجح يكفي |
الفخ ④ — await على حاجة المستخدم مش محتاج يستناها
لو عندك خطوة ضرورية للـ Request (زي خصم الفلوس)، لازم تستناها بـ await. لو عندك خطوة تبعية (زي كتابة Log أو Audit) والـ Client مش محتاج نتيجتها عشان يشوف الرد، ما توقفش الرد عليها — خليها تشتغل في الخلفية مع .catch() عشان مفيش Unhandled Rejection.
// ❌ بطيء — المستخدم بيستنى الـ Log يتكتب في الـ DB قبل ما يشوف النتيجة:
async function processOrder(order) {
const result = await chargeCard(order.amount); // لازم تستناه
await auditLog.write(result); // ليه؟ المستخدم مش محتاجه!
return result;
}
// ✅ أسرع — Log بيحصل في الخلفية:
async function processOrder(order) {
const result = await chargeCard(order.amount);
auditLog.write(result).catch(err => // مش await — بيشتغل جنب
logger.error('Audit log failed', err) // بس مع catch!
);
return result; // ← بيرجع للمستخدم أسرع
}
إيه الفرق: في النسخة الأولى، الرد على الـ Client بيتبعت بعد ما الـ audit log يخلص. لو الـ DB بتاعة الـ Log بطيئة، الـ Response يتأخر من غير فايدة للعميل. في النسخة التانية، auditLog.write(result) بتبدأ وتمشي من غير await — والـ function بترجع result فوراً. الـ Log بيتم في الخلفية. الـ .catch() ضروري عشان لو الـ write فشل، الـ Error تتسجل وماتبقاش Unhandled Rejection (نفس درس الـ Refund).
تحذير: الـ “Fire and Forget” (بدون await مع وجود .catch) مقبول للـ Logging والـ Analytics والـ Audit اللي مش جزء من “نجاح الطلب من وجهة العميل”. ما تعملهاش مع أي خطوة بتغيّر حالة النظام وتعتمد عليها الخطوات اللي بعدها — زي حفظ الأوردر في الـ DB أو تحديث الرصيد. دي لازم تبقى awaited وتتعامل مع نتيجتها؛ عشان لو فشلت تعرف ترد وترجع أو تعمل تعويض.
💡 قبل ما تكمل — الـ Middleware: الـ
asyncWrapperوالـglobalErrorHandlerوالـnextاللي جايين — دول تطبيقات لفكرة واحدة اسمها Middleware Pipeline. لو مش فاهمها اقرأ المقالة المخصصة دي الأول. لو فاهمها — كمّل.
5. منظومة الـ Error Handling: من الفوضى للنظام
الجزء ده بيجمع كل اللي فات: بدل ما كل endpoint يمسك الـ Errors بطريقته وكل فريق يكتب رسائل مختلفة، نبني منظومة واحدة — كل خطأ له نوع، كل نوع له Status Code ورسالة، والـ Handler نفسه مافيهوش تكلف. النتيجة: كود أنضف، ردود متسقة للـ Client، وفرق واضح بين “غلط متوقع من المستخدم” و”Bug في السيرفر”. نبدأ من المشكلة اللي المنظومة دي بتحلها، بعدين الحل خطوة خطوة.
المشكلة: الـ if/else سلسلة لا نهاية لها
لو كل الـ Error handling عندك عبارة عن try/catch في كل route، وكل catch فيه سلسلة if (err.message === '...') عشان تحدد الـ Status Code والرسالة، هتواجه اتنين: تكرار وهشاشة.
التكرار: تخيل 10 Endpoints — تسجيل، دخول، تعديل بروفايل، إنشاء أوردر، إلخ. في كل واحد تكتب نفس النمط:
} catch (err) {
if (err.message === 'NOT_FOUND') res.status(404).json({ error: 'مش موجود' });
if (err.message === 'EMAIL_EXISTS') res.status(409).json({ error: 'الإيميل مسجل' });
if (err.message === 'INVALID_DATA') res.status(400).json({ error: 'داتا غلط' });
else res.status(500).json({ error: 'حصل خطأ في السيرفر' });
}
يعني نفس الشروط في 10 أماكن. لو غيّرت رسالة أو أضفت نوع خطأ جديد (مثلاً RATE_LIMIT)، تعدّل في 10 ملفات. ولو نسيت endpoint واحد، الـ Client هياخد 500 بدل الرد الصح — ومش هتلاقي المشكلة بسرعة.
الهشاشة: الاعتماد على نص الـ message (err.message === 'NOT_FOUND') خطر. الـ message ممكن يتغيّر من مطور للتاني، أو يتكتب بحروف مختلفة:
// في الـ Service بتاع زميلك:
throw new Error('user not found'); // بحروف صغيرة
// في الـ Controller بتاعك:
if (err.message === 'NOT_FOUND') { // مش هيتطابق أبداً
res.status(404).json({ ... });
}
// النتيجة: الـ Error بيمر على كل الـ if ويروح لـ else → 500
يعني نفس الخطأ المنطقي (اليوزر مش موجود) بياخد 404 في مكان و 500 في مكان تاني — والفرق بس في طريقة كتابة الـ string. عشان كده الحل مش إننا “نوحد النص”، الحل إننا ما نعتمدش على النص أصلاً — نعتمد على نوع الـ Error (كلاس).
الحل: Custom Error Classes — اتكلم بـ instanceof مش بـ Strings
بدل ما نرمي new Error('NOT_FOUND') ونقارن الـ message، نعرّف كلاسات لكل نوع خطأ. كل كلاس له اسم ثابت (مثلاً NotFoundError)، وله statusCode و code جواه. في الـ Handler بنسأل: err instanceof NotFoundError — مش بنقارن strings. كده أي حد يرمي new NotFoundError('User') الرد هيبقى 404 في كل المشروع، من غير ما يتنسى أو يختلف النص.
الكلاس الأساسي — AppError:
كل أخطاء التطبيق (اللي احنا نرميها عمداً من قواعد الـ Business) ترث من AppError. الـ AppError بيخزن: الـ message، الـ statusCode (400، 404، 409، …)، والـ code (string للـ Client زي NOT_FOUND). وبيخزن isOperational = true — معناها “ده خطأ متوقع من قواعد الشغل، مش Bug في الكود”. الـ name بنحطه من اسم الـ constructor عشان في الـ Logs تعرف النوع. والـ Error.captureStackTrace عشان الـ stack trace يبدأ من مكان الـ throw مش من داخل الـ Error class.
// errors/AppError.js — الأساس: كل أخطاء التطبيق ترث منها
class AppError extends Error {
constructor(message, statusCode, code, isOperational = true) {
super(message);
this.statusCode = statusCode;
this.code = code;
this.isOperational = isOperational; // ← دي هنشرحها بعد شوية
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AppError;
الأنواع المتخصصة:
كل نوع خطأ HTTP له كلاس: Validation (400)، NotFound (404)، Conflict (409)، Unauthorized (401)، Forbidden (403). كل واحد يستدعي super بالـ message والـ statusCode والـ code المناسب. الـ ValidationError ممكن يضيف field عشان الـ Client يعرف أي حقل فيه المشكلة.
// errors/index.js — الأنواع المتخصصة
const AppError = require('./AppError');
class ValidationError extends AppError {
constructor(message, field = null) {
super(message, 400, 'VALIDATION_ERROR');
this.field = field; // إيه الـ Field المشكلة فيه بالظبط
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} مش موجود`, 404, 'NOT_FOUND');
}
}
class ConflictError extends AppError {
constructor(message) { super(message, 409, 'CONFLICT'); }
}
class UnauthorizedError extends AppError {
constructor(message = 'محتاج تسجل دخول') {
super(message, 401, 'UNAUTHORIZED');
}
}
class ForbiddenError extends AppError {
constructor(message = 'مش مسموح ليك') {
super(message, 403, 'FORBIDDEN');
}
}
module.exports = { AppError, ValidationError, NotFoundError, ConflictError, UnauthorizedError, ForbiddenError };
الـ Service بعد التحول:
بدل throw new Error('NOT_FOUND') تكتب throw new NotFoundError('User'). الـ Controller (والـ Global Handler لاحقاً) مش محتاج يعرف الـ message — يكفيه err instanceof NotFoundError أو يستخدم err.statusCode و err.code اللي جوا الـ Error. الـ Service بيبقى واضح: أي شرط business مش متحقق، نرمي الـ Error المناسب. مفيش string comparison في أي حتة.
// services/user.service.js
const { NotFoundError, ConflictError, ValidationError } = require('../errors');
const userService = {
create(data) {
if (!data.email?.includes('@'))
throw new ValidationError('الإيميل مش صالح', 'email');
if (users.find(u => u.email === data.email))
throw new ConflictError('الإيميل ده مسجل قبل كده');
const user = { id: nextId++, ...data, role: data.role ?? 'user' };
users.push(user);
return user;
},
getById(id) {
const user = users.find(u => u.id === id);
if (!user) throw new NotFoundError('User'); // واضح ومش قابل للغلط في الكتابة
return user;
},
};
المفهوم الأساسي: isOperational — مش كل Error متساوية
في Production مش كل الـ Errors بنفس الطبيعة. في أخطاء تشغيلية (Operational) — جزء من قواعد الشغل، متوقعة، المستخدم يقدر يصلحها. وفي أخطاء مبرمج (Programmer Errors) — Bug في الكود، مش متوقعة، والسيرفر ممكن يكون في حالة غلط.
Operational: مثلاً اليوزر دخل ID مش موجود → NotFoundError. بعت فورم من غير إيميل → ValidationError. الإيميل مسجل قبل كده → ConflictError. السيرفر يرد برسالة الخطأ المناسبة (404، 400، 409)، والـ Request يخلص، والسيرفر يكمل شغله. مفيش حاجة “مكسورة” في الكود — بس الشروط متحققتش.
Programmer Error: مثلاً كود كتب users[undefined].name → TypeError. أو استدعاء خاطئ لـ API داخلي → SyntaxError أو ReferenceError. ده معناه إن في سطر غلط في الكود. السيرفر ما يردش بنفس تفاصيل الـ Error للـ Client (ممكن تكشف مسارات وملفات). نرد “حصل خطأ في السيرفر” بس، ونسجّل الـ Error الحقيقي في الـ Logs، ونبعته للـ Team (Slack / PagerDuty). والأأمن إن الـ Process يعمل Restart عشان الـ state الداخلي ممكن يكون اتلوث.
ليه الفرق ده مهم:
الأمان: الـ Programmer Errors غالباً فيها stack trace — أسماء ملفات، دوال، أحياناً متغيرات. لو بعت ده للـ Client، حد ضار يقدر يستنتج بنية المشروع. الـ Operational Errors احنا بنحدد رسالتها — فما نبعثش إلا اللي آمن.
الإنذارات: لو كل Error (حتى 404) بيوقظ الـ Engineer، الإنذارات بتبقى ضجيج وهو هيقلل يهتم. لو بس الـ Programmer Errors هي اللي بتوقظه، يعرف إن في Bug فعلي محتاج يتصلح.
الـ Recovery: بعد
NotFoundErrorالسيرفر شغال طبيعي. بعد TypeError في مكان غلط، الـ Process ممكن يكون عدّى على state غلط — الـ Restart يضمن بداية نظيفة.
الـ isOperational في الـ AppError معناه: “ده خطأ احنا رمناه من قواعد الـ Business، مش Exception من جوا الـ Runtime.” الـ Global Handler يستخدمه عشان يفرق: لو operational يرد الـ message والـ statusCode بتوع الـ Error؛ لو غير كده يرد 500 ورسالة عامة ويسجّل الحقيقة ويبعتها للـ Team.
الـ Global Error Handler: البرج المركزي
الـ Global Error Handler هو الـ Middleware اللي كل الـ Errors بتوصلله في الآخر. Express بيستدعيه لما حد ينادي next(err). وظيفته أربع حاجات: تسجيل الـ Error، تحديد هل هو operational ولا programmer، تنبيه الـ Team لو programmer، ورد آمن على الـ Client بدون تسريب معلومات.
① التسجيل: كل Error — سواء operational أو programmer — لازم يتسجّل. عشان نستنى الـ Logs نعرف إيه اللي حصل. الفرق إن الـ Operational بنسجّلها كـ warn (مش خطأ في الكود، بس حصلت حالة متوقعة). الـ Programmer بنسجّلها كـ error. الـ stack بنبعته في الـ Logs في الـ development عشان نصلح الـ Bug؛ في الـ Production غالباً ما نبعثش الـ stack للـ Client، لكن بنسجّله داخلياً.
② تحديد النوع: const isOperational = err instanceof AppError && err.isOperational. لو الـ Error من كلاس احنا معرفينه (ورث من AppError) و isOperational = true، يبقى خطأ تشغيلي. أي حاجة تانية (TypeError، Error من مكتبة، إلخ) نعتبرها programmer error.
③ تنبيه الـ Team: لو !isOperational نعمل الخطوة اللي تناسبك — مثلاً sendAlertToSlack(err) أو إرسال لـ PagerDuty. الفكرة إن الـ Engineer يتنبه فوراً للـ Bug.
④ الرد على الـ Client: لو operational نستخدم err.statusCode و err.message و err.code. لو programmer نستخدم 500 ورسالة عامة (“حصل خطأ في السيرفر”) و code زي INTERNAL_ERROR. الـ ValidationError ممكن يكون فيها field — نضيفه في الـ JSON لو موجود. الـ stack ما نبعثهوش للـ Client في Production؛ في development ممكن نضيفه عشان الـ Debug. استخدام res.writeHead و res.end هنا يضمن إن الـ Response يتبعت مرة واحدة حتى لو في حاجة اتغيرت قبل كده في الـ res.
// middleware/errorHandler.js
const { AppError } = require('../errors');
function globalErrorHandler(err, req, res) {
// ① سجّل الـ Error دايماً — حتى لو مش هنبعته للـ Client
const logLevel = err.isOperational ? 'warn' : 'error';
console[logLevel]({
type: err.name || 'Error',
message: err.message,
code: err.code,
url: `${req.method} ${req.url}`,
timestamp: new Date().toISOString(),
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
});
// ② هل ده خطأ تشغيلي متوقع ولا Bug؟
const isOperational = err instanceof AppError && err.isOperational;
// ③ الـ Programmer Errors تستحق Alert فوري للـ Team
if (!isOperational) {
// في Production: sendAlertToSlack(err) أو PagerDuty
console.error('🚨 PROGRAMMER ERROR — يحتاج تدخل فوري!');
}
// ④ رد آمن على الـ Client — بدون تسريب
const statusCode = isOperational ? err.statusCode : 500;
const message = isOperational ? err.message : 'حصل خطأ في السيرفر';
const code = isOperational ? err.code : 'INTERNAL_ERROR';
res.writeHead(statusCode, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({
success: false,
error: {
message,
code,
...(err.field && { field: err.field }),
...(process.env.NODE_ENV === 'development' && err.stack && { stack: err.stack }),
},
}));
}
module.exports = { globalErrorHandler };
الـ asyncWrapper: الجسر بين الـ Handlers والـ Global Error Handler
في Express، الـ sync middleware لو رمت Error، Express بيمسكها ويستدعي الـ Error middleware (اللي انت سجلته بـ app.use(globalErrorHandler)). لكن الـ async handlers بترجع Promise. لو الـ Promise اترفضت (يعني في الـ handler حصل throw أو rejection)، Express مش بيمسكها — الـ Error بتضيع أو تسبب Unhandled Rejection. عشان كده محتاج جسر: نلّف كل async handler في function تمسك أي Rejection من الـ Promise وتمررها لـ next(err) عشان Express يوصلها للـ globalErrorHandler.
إزاي الـ asyncWrapper بيشتغل: الـ asyncWrapper(fn) بيرجع function بتاخد (req, res, next). جواها بنستدعي الـ handler: fn(req, res, next). الـ fn دي async فبترجع Promise. لو الـ Promise fulfilled، مفيش حاجة نعملها. لو رمت أو اترفضت، الـ .catch(next) بيمسك الـ Error ويستدعي next(err). والـ next في Express للـ Error middleware هو الـ globalErrorHandler. فكل Error من جوا الـ handler بتوصل للـ global handler بالظبط زي ما لو الـ handler كان sync ورمى.
// utils/asyncWrapper.js
// بيلف أي async handler ويضمن إن أي Error تتصعّد للـ Global Handler
function asyncWrapper(fn) {
return function(req, res, next) {
Promise.resolve(fn(req, res, next)).catch(next);
// ↑ next = globalErrorHandler
};
}
module.exports = { asyncWrapper };
الخلاصة: الـ Handler يرمي الـ Error المناسب (مثلاً throw new NotFoundError('User')). الـ asyncWrapper يمسك أي Rejection ويستدعي next(err). الـ globalErrorHandler يحدد الـ statusCode والرسالة ويرد للـ Client ويسجّل ويبعث تنبيه لو programmer error. كل الـ Error handling في مكان واحد، والـ Handlers نظيفة.
6. التطبيق الكامل: المشروع بعد إضافة المنظومة
هيكل المشروع النهائي
📁 my-api/
│
├── 📁 errors/
│ ├── AppError.js ← Base Class
│ └── index.js ← ValidationError, NotFoundError, ConflictError, ...
│
├── 📁 middleware/
│ └── errorHandler.js ← Global Error Handler — نقطة تجميع واحدة
│
├── 📁 utils/
│ ├── asyncWrapper.js ← الجسر بين الـ Handlers والـ Global Handler
│ ├── parseBody.js ← قراءة الـ Body
│ └── response.js ← sendResponse Helper
│
├── 📁 routes/
├── 📁 controllers/ ← مفيش try/catch — asyncWrapper بيتكفل
├── 📁 services/ ← بترمي Custom Errors بدل Error عادية
└── server.js ← التجميع + الحمايات النهائية
الـ Controller النهائي — بدون try/catch
// controllers/user.controller.js
const { asyncWrapper } = require('../utils/asyncWrapper');
const { parseBody } = require('../utils/parseBody');
const { sendResponse } = require('../utils/response');
const { ValidationError } = require('../errors');
const userService = require('../services/user.service');
// دالة validation مركزية — بترمي ValidationError لو في مشكلة
function requireUserFields(body) {
if (!body) throw new ValidationError('الـ Body مطلوب');
if (!body.name?.trim()) throw new ValidationError('الاسم مطلوب', 'name');
if (!body.email?.trim()) throw new ValidationError('الإيميل مطلوب', 'email');
if (!body.email.includes('@')) throw new ValidationError('الإيميل مش صالح', 'email');
}
const userController = {
getAllUsers: asyncWrapper(async (req, res) => {
const page = Math.max(parseInt(req.query?.page) || 1, 1);
const limit = Math.min(parseInt(req.query?.limit) || 10, 100);
const result = userService.getAll(page, limit);
sendResponse(res, 200, { success: true, ...result });
}),
getUserById: asyncWrapper(async (req, res) => {
const user = userService.getById(req.params.id);
// لو مش موجود: userService.getById بترمي NotFoundError تلقائياً
sendResponse(res, 200, { success: true, data: user });
}),
createUser: asyncWrapper(async (req, res) => {
const body = await parseBody(req);
requireUserFields(body); // ValidationError لو فيه مشكلة
const user = userService.create(body); // ConflictError لو الإيميل موجود
sendResponse(res, 201, { success: true, data: user });
}),
updateUser: asyncWrapper(async (req, res) => {
const body = await parseBody(req);
requireUserFields(body);
const user = userService.update(req.params.id, body);
sendResponse(res, 200, { success: true, data: user });
}),
deleteUser: asyncWrapper(async (req, res) => {
const deleted = userService.remove(req.params.id);
sendResponse(res, 200, { success: true, message: `تم حذف "${deleted.name}"` });
}),
};
module.exports = userController;
لاحظ: مفيش try/catch واحد. كل Error — سواء ValidationError من requireUserFields أو ConflictError من الـ Service أو أي Bug غير متوقع — بتتصعّد لـ asyncWrapper ثم للـ globalErrorHandler أوتوماتيكياً. وكل مرة نفس الـ Format، نفس الـ Structure.
الحمايات النهائية في server.js
// server.js — الدرع الكامل
// ① شبكة الأمان الأخيرة: Promise مترفضة من غير catch (فاكر قصة الـ refund؟)
process.on('unhandledRejection', (reason, promise) => {
console.error('🚨 Unhandled Promise Rejection:', reason);
// في Production: ابعت Alert وأوقف السيرفر بشكل آمن
// server.close(() => process.exit(1));
});
// ② شبكة الأمان الأخيرة: Exception في Sync Code
process.on('uncaughtException', (err) => {
console.error('💥 Uncaught Exception — السيرفر في state غير معروفة:', err);
// لازم Restart — السيرفر ممكن يكون تالف
server.close(() => process.exit(1));
});
// ③ إغلاق آمن (Kubernetes / Docker بيبعت SIGTERM قبل الإيقاف)
process.on('SIGTERM', () => {
console.log('🛑 بدأ الإغلاق الآمن...');
server.close(() => {
console.log('✅ كل الاتصالات خلصت — السيرفر اتوقف بأمان');
process.exit(0);
});
// لو بعد 30 ثانية لسه الاتصالات ما خلصتش — أجبر الإغلاق
setTimeout(() => process.exit(1), 30_000);
});
const PORT = process.env.PORT || 3000;
const server = http.createServer(handleRequest);
server.listen(PORT, () => console.log(`✅ http://localhost:${PORT}`));
7. التشبيه الكبير: المحكمة والقضاة
عشان الـ Mental Model يكمل معاك:
كل Request (الدعوى): بتيجي للمحكمة (السيرفر) وبتمشي بشكل منظم. عندها حق تنجح أو ترفض — بس لازم يكون في رد.
الـ Promise (الخبير المحلف): القاضي مش بيروح بنفسه يجيب كل الأدلة. بيكلف خبراء (الـ Promises) ويقول “انت اجيب التقرير وانبهني”. الخبير بيروح يشتغل والقاضي بيسمع قضايا تانية في نفس الوقت. لما الخبير يرجع — يكمل.
بدون
.catch(خبير بيختفي بصمت): لو الخبير راح وما رجعش ومحدش سأل عنه — القضية مستنياه، المحكمة مش عارفة إيه اللي حصل، والوقت بيضيع. ده الـ Unhandled Rejection.الـ Custom Errors (رموز المحكمة الرسمية): مش بيقولوا “في مشكلة” وخلاص. في كود محدد:
406-A= الوثائق ناقصة،409-B= تعارض في الطلبات. كل كود معاه بروتوكول محدد — زيthrow new ValidationError(...).الـ isOperational (نوع الرفض): “الدعوى اترفضت لأن الوثائق ناقصة” (Operational — القاضي يشرح للمدعي إيه الناقص) VS “القاضي لقى إن أصلاً الـ Case File مش موجود في النظام بسبب خطأ داخلي” (Programmer Error — مش المدعي مسؤول، المحكمة عندها مشكلة داخلية).
الـ Global Error Handler (رئيس المحكمة): مش كل قاضي بيحل مشاكله لوحده. في رئيس بيشوف كل القضايا وبيحدد: “ده رفض عادي، وضّحوا للمدعي السبب. ده خطأ داخلي، وقفوا الجلسات وصلحوا النظام.”
الـ
asyncWrapper(المحضر القضائي): بيضمن إن مفيش قضية بتتهرب من النظام. أي Exception — حتى الـ Unexpected — بيمسكها ويوديها لرئيس المحكمة بدل ما تختفي.
8. ساحة الاختبار (The Crucible)
أ) أسئلة الانترفيو القاتلة
١ — لغز الـ async المتداخلة:
async function outer() {
async function inner() {
throw new Error('من inner');
}
inner(); // ← مش فيه await ولا .catch
return '✅ outer خلص';
}
outer()
.then(result => console.log(result))
.catch(err => console.log('❌ Error:', err.message));
إيه اللي هيطلع؟ وليه؟
إجابة: هيطلع
✅ outer خلص— وبعدين في الـ Console هيطلعUnhandledPromiseRejectionلـ Error منinner. ليه؟ لأنinner()بترجع Promise مرفوضة ومحدش مسك الـ reject بتاعها. الـ.catchعلىouterمش هيمسكها لأنouterنفسها ما رمتش Error — هي رجعت ’✅ outer خلص’ بنجاح. الـ Error في Promise منفصلة ما ربطتش بـouter.
٢ — لغز الـ Promise.allSettled التسلسلية:
const delay = (ms, fail = false) =>
new Promise((res, rej) =>
setTimeout(() => fail ? rej(new Error('failed')) : res('ok'), ms)
);
const results = await Promise.allSettled([
delay(100), // ✅ بعد 100ms
delay(50, true), // ❌ بعد 50ms
delay(200), // ✅ بعد 200ms
]);
console.log('انتهى بعد كام ms؟');
console.log('النتيجة التانية:', results[1].status);
إجابة: انتهى بعد 200ms (أطول Promise). رغم إن التانية فشلت في 50ms، الـ
allSettledبيستنى كلهم — مش بيقف عند أول فشل. النتيجة التانية:'rejected'. الـresults[1].reasonفيه الـ Error object.
٣ — سؤال الـ isOperational في الانترفيو:
ليه الـ error.isOperational = true أهم من مجرد التحقق من err instanceof AppError؟
إجابة:
instanceofبيتحقق من النوع بس — بس أحياناًAppErrorيمكن يستخدمها كـ “Wrapper” لـ Programmer Error. الـisOperationalFlag هي قرار صريح من المطور وقت رمي الـ Error: “أنا المسؤول عن الكود ده، وأنا عارف إن الـ Error دي جزء من السيناريوهات المتوقعة أو لأ”. ممكن يكون عندكAppErrorبـisOperational = false— لو اتكشف خلال التشغيل إن في Bug في الـ Logic بتاعك بالرغم إنك استخدمت Custom Error Class.
9. خلاصة اليوم — الـ Mental Model الكامل
من الآخر: الـ Async مش Syntax تكتبها وتكمل. الـ Promise ليها دورة حياة واضحة — Pending ثم Fulfilled أو Rejected — وإهمال الـ Rejection مش بيعطيك Error واضحة فوراً، بيضيع صامتاً ويظهر في الـ Production بعد أسابيع في شكل مش متوقع.
الـ Custom Errors بتخلي الكود يتكلم بلغة Business حقيقية —
throw new NotFoundError('User')أوضح وأأمن منthrow new Error('NOT_FOUND'). والـisOperationalFlag هي الفرق بين “هاددي للمستخدم رسالة الخطأ” و”فيه Bug يستحق إيقاظ الـ Team”.والـ Global Error Handler مش مجرد “catch كل حاجة”. هو العقد بين الـ Backend والـ Frontend: نفس الـ Format، نفس الـ Structure، في كل الحالات — نجاح أو فشل. وبدله تكتب
try/catchفي 100 مكان، مكان واحد في الـ Middleware بيكفي.
في اليوم الجاي هندخل في قواعد البيانات والـ SQL — نبدأ نخزن الداتا الحقيقية ونتعلم إزاي نفكر في تصميم الـ Schema من البداية.
