تخطَّ إلى المحتوى

١٣ يوليو ٢٠٢٦

حل خطأ Maximum call stack size exceeded بالعربي

شارك المقال:

تشغّل الكود فينفجر محرك جافاسكريبت برسالة قصيرة حاسمة:

RangeError: Maximum call stack size exceeded

هذا هو «Stack Overflow» الحرفي — الخطأ الذي سُمي أشهر موقع برمجي في العالم على اسمه 😄. ومعناه شبه الدائم: دالة تستدعي نفسها إلى الأبد.

اقرأ الـ Stack Trace — التشخيص في ثوانٍ

وسّع الخطأ في الـ console وسترى المشهد المميز:

RangeError: Maximum call stack size exceeded
    at countDown (app.js:2)
    at countDown (app.js:3)
    at countDown (app.js:3)
    at countDown (app.js:3)
    ... (آلاف المرات)

نفس الاسم مكررًا بلا نهاية = هو المذنب. وإن رأيت اسمين يتناوبان (a ثم b ثم a...) فهو تكرار متبادل بين دالتين.

الأسباب الأربعة الشائعة

السببالشكلالحل
recursion بلا حالة أساسدالة عدّ تنسى شرط التوقفأضف base case يعيد قيمة
حالة أساس لا تُبلغ أبدًاالعداد يبتعد عن الشرط بدل الاقترابتأكد أن كل استدعاء «يقترب»
استدعاء متبادلa تستدعي b وb تستدعي aاقطع الحلقة بشرط
getter/setter يستدعي نفسهget name() { return this.name }خزّن في اسم مختلف _name

1) نسيان حالة الأساس — الكلاسيكية

// ❌ متى تتوقف؟ أبدًا
function countDown(n) {
  console.log(n);
  countDown(n - 1);
}

// ✅ حالة الأساس أولًا — باب الخروج
function countDown(n) {
  if (n < 0) return;   // شرط التوقف
  console.log(n);
  countDown(n - 1);
}

قاعدة كل recursion سليم: حالة أساس تعيد قيمة، وكل استدعاء يقترب منها.

2) الحالة موجودة... لكنها لا تُبلَغ

// ❌ الشرط n === 0 لكن نبدأ بـ 5.5 — سنقفز فوقه للأبد
function halve(n) {
  if (n === 0) return;
  halve(n - 1);       // 5.5 → 4.5 → ... → 0.5 → -0.5 → ...
}

الحل: اجعل الشرط «شبكة» لا «نقطة»: if (n <= 0) return; — الشروط المفتوحة (<=، >=) أأمن من المساواة الصارمة في التوقف.

3) getter يستدعي نفسه — الأخفى

// ❌ قراءة this.name داخل getter لـ name = استدعاء ذاتي أبدي
class User {
  get name() { return this.name; }
  set name(v) { this.name = v; }   // نفس الكارثة
}

// ✅ التخزين باسم داخلي مختلف
class User {
  get name() { return this._name; }
  set name(v) { this._name = v; }
}

نفس الفخ يظهر في React: مكوّن يرندر نفسه داخل نفسه، أو setState يشغّل effect يعيد setState — قريبه المباشر.

4) نشر مصفوفة ضخمة (الحالة النادرة بلا recursion)

// ❌ كل عنصر يصبح وسيطًا في المكدس
Math.max(...millionNumbers);

// ✅ حلقة أو reduce
millionNumbers.reduce((m, x) => (x > m ? x : m), -Infinity);

قائمة تحقّق سريعة

  • وسّع الـ stack trace: اسم متكرر؟ وجدت المذنب.
  • لكل recursion: حالة أساس تعيد + كل استدعاء يقترب منها.
  • شروط توقف بـ <=/>= لا بمساواة صارمة قد تُقفز.
  • getters/setters: الاسم الداخلي _name غير اسم الخاصية.
  • عمق ضخم مشروع؟ حوّله لحلقة بمكدس يدوي.

الخلاصة

Maximum call stack size exceeded صرخة المحرك: «أحدهم يستدعي نفسه بلا نهاية». افتح الـ trace، اقرأ الاسم المتكرر، وأصلح شرط التوقف — دقيقة تشخيص تحل ما يبدو أعقد أخطاء جافاسكريبت.

أتقن الدوال من مسار جافاسكريبت بالعربي والتكرار من مسار هياكل البيانات، وأكمل مع حل حلقة useEffect اللانهائية و جدول أخطاء TypeError.

📚 مصادر رسمية للتعمّق: توثيق JavaScript على MDN

الأسئلة الشائعة

ما هو الـ call stack أصلًا وبكم يفيض؟

مكدس يسجل فيه المحرك «أين كنا» عند كل استدعاء دالة، ويُفرغ تباعًا عند كل return. كل استدعاء متداخل يضيف إطارًا؛ وحين تتراكم عشرات الآلاف من الإطارات دون return (تكرار لا نهائي عادة) يفيض المكدس فيرمي المحرك هذا الخطأ حمايةً للذاكرة. الحد يختلف بين المتصفحات (~10-50 ألف إطار تقريبًا).

هل يظهر الخطأ بلا recursion أصلًا؟

نادرًا نعم: تمرير مصفوفة ضخمة (مئات الآلاف) كوسائط مفردة مثل Math.max(...hugeArray) يفجّر المكدس لأن كل عنصر يصبح وسيطًا. الحل استخدام حلقة أو reduce بدل النشر. لكن في 95% من الحالات ابحث عن الاستدعاء الذاتي أولًا.

أحتاج recursion فعلًا لعمق كبير — ما الحل؟

حوّله لحلقة تكرارية بمكدس يدوي (مصفوفة تدفع إليها العناصر وتسحب منها) — نفس المنطق بلا حدود المكدس الحقيقي. هذا النمط القياسي لاجتياز أشجار عميقة جدًا كملفات نظام ضخمة.

اقرأ أيضًا

تصفّح كل المقالات في المدوّنة، أو ابدأ التعلّم من المسارات و خرائط الطريق.