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

١٣ يوليو ٢٠٢٦ · آخر تحديث: ١٩ أكتوبر ٢٠٢٦

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

شارك المقال:

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

RangeError: Maximum call stack size exceeded

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

ما هو مكدس الاستدعاءات؟

كل مرة تستدعي دالة، يضع المحرك «إطارًا» فوق المكدس يتذكّر فيه أين كان، ليعود إليه عند return. دالة تستدعي دالة تستدعي دالة: ثلاثة إطارات فوق بعض، تُزال بالترتيب العكسي.

المكدس محدود الحجم. قسنا دالة فارغة تستدعي نفسها بلا توقف:

let depth = 0;
function deep() { depth++; deep(); }
try { deep(); } catch (e) { console.log(depth, e.message); }
// Node.js 22:  12554  Maximum call stack size exceeded
// Chromium:    12476  Maximum call stack size exceeded

حوالي 12,500 استدعاء، وأقل مع دوال أثقل. عند تجاوزه يوقف المحرك البرنامج بدل أن يأكل الذاكرة كلها. والمحرك يرميه كـ RangeError، فتستطيع التقاطه بـ try/catch، لكن التقاطه علاج للعرَض لا للسبب.

اقرأ الـ stack trace: التشخيص في ثوانٍ

وسّع الخطأ في الـ console:

RangeError: Maximum call stack size exceeded
    at countDown (app.js:2:3)
    at countDown (app.js:3:3)
    at countDown (app.js:3:3)
    at countDown (app.js:3:3)
    ...

الاسم المكرر هو الجاني، ورقم السطر المتكرر (app.js:3) هو مكان الاستدعاء الذاتي. وإن رأيت اسمين يتناوبان (a ثم b ثم a) فهو تكرار متبادل بين دالتين.

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

السببالشكلالحل
recursion بلا حالة أساسدالة تنسى شرط التوقفأضف base case يعيد قيمة
حالة أساس لا تُبلغ أبدًاالعداد يتجاوز الشرط<= بدل ===، وتأكد أن كل استدعاء يقترب
استدعاء متبادلa تستدعي b وb تستدعي aاقطع الحلقة بشرط
getter أو setter يستدعي نفسهget name() { return this.name }خزّن في _name أو #name
كائن دائريدالة نسخ أو مرور على obj.self = objstructuredClone أو تتبّع ما زرته
نشر مصفوفة ضخمةMath.max(...hugeArray)حلقة أو reduce
recursion صحيح لكن عميق جدًامجموع أو مرور على 100 ألف مستوىحوّله لحلقة

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 step(n) {
  if (n === 0) return;
  step(n - 1);       // 5.5 → 4.5 → … → 0.5 → -0.5 → -1.5 → …
}

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

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

class User {
  get name() { return this.name; }   // قراءة name داخل getter الخاص بـ name
}
new User().name;   // RangeError: Maximum call stack size exceeded

جرّبناه فانفجر فورًا. داخل get name()، الكلمة this.name تستدعي الـ getter نفسه. والـ setter بالعكس: set name(v) { this.name = v; } يستدعي نفسه عند كل إسناد. خزّن القيمة باسم مختلف:

class User {
  #name;                                  // حقل خاص
  get name() { return this.#name; }
  set name(v) { this.#name = v.trim(); }
}

(الأصناف والحقول الخاصة في درس الأصناف.)

قريبه في React: مكوّن يحدّث الحالة في كل عرض، أو effect يحدّث حالة هي نفسها في مصفوفة الاعتماديات. هذه لها رسائلها الخاصة غالبًا، وشرحها في حلقة useEffect اللانهائية.

4) كائن دائري

const a = { name: "a" };
a.self = a;                  // الكائن يشير لنفسه

function clone(o) {          // نسخ عميق يدوي
  if (typeof o !== "object" || o === null) return o;
  const r = {};
  for (const k in o) r[k] = clone(o[k]);
  return r;
}
clone(a);                    // RangeError: Maximum call stack size exceeded

الدالة تنزل في a.self.self.self… بلا نهاية. جرّبنا البديل المدمج structuredClone(a) فنسخه بلا مشكلة، وحافظ على المرجع الدائري في النسخة. وإن كتبت دالة مرور بنفسك، فاحفظ ما زرته في WeakSet وتخطَّه.

ولاحظ الفرق: JSON.stringify(a) لا يعطي هذا الخطأ، بل TypeError: Converting circular structure to JSON. الرسالة تختلف والسبب واحد.

5) نشر مصفوفة ضخمة: بلا recursion إطلاقًا

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

قسنا في Node 22: نجح مع 120 ألف عنصر، وفشل مع 130 ألفًا بالرسالة نفسها. والحد يختلف بين المحركات، فلا تعتمد عليه:

const max = numbers.reduce((m, x) => (x > m ? x : m), -Infinity);

ونفس الفخ مع arr.push(...otherHugeArray): استخدم حلقة، أو arr = arr.concat(other).

6) recursion صحيح، لكن أعمق من المكدس

function sumTo(n) { return n === 0 ? 0 : n + sumTo(n - 1); }
sumTo(5000);      // 12502500
sumTo(100000);    // RangeError: Maximum call stack size exceeded

الدالة سليمة، والحالة الأساسية موجودة، لكن 100 ألف مستوى أكثر مما يتسع له المكدس. الحل حلقة:

function sumTo(n) {
  let s = 0;
  for (let i = 1; i <= n; i++) s += i;
  return s;
}

وللمرور على شجرة عميقة (مجلدات، أو DOM، أو JSON متداخل) استخدم مكدسًا يدويًا:

function walk(root, visit) {
  const stack = [root];
  while (stack.length) {
    const node = stack.pop();
    visit(node);
    for (const child of node.children ?? []) stack.push(child);
  }
}

نفس المنطق، لكن المكدس مصفوفة عادية حجمها حجم الذاكرة لا حجم مكدس المحرك. (جافاسكريبت لا تطبّق «tail call optimization» عمليًا في V8، فلا تعتمد عليها.)

أما خيار node --stack-size فيرفع الحد (جرّبنا --stack-size=2000 فوصلنا لحوالي 25,500)، لكنه حل مؤقت قد يُسقط العملية كلها إن تجاوز حدود النظام. حوّل الكود لحلقة.

في اللغات الأخرى

اللغةالرسالة
Chrome وNode.js وEdgeRangeError: Maximum call stack size exceeded
FirefoxInternalError: too much recursion
PythonRecursionError: maximum recursion depth exceeded (الحد الافتراضي 1000)
Javajava.lang.StackOverflowError

جرّبنا بايثون: sys.getrecursionlimit() أعاد 1000، والدالة اللانهائية أعطت الرسالة أعلاه. يمكن رفعه بـ sys.setrecursionlimit، لكن الحل الصحيح نفسه: حالة أساس، أو حلقة.

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

  • وسّع الـ stack trace: اسم ورقم سطر متكرر = الجاني.
  • لكل recursion: حالة أساس تعيد، وكل استدعاء يقترب منها، بـ <= لا ===.
  • getter أو setter؟ خزّن في #name أو _name.
  • كائن يشير لنفسه؟ structuredClone أو WeakSet للمزور.
  • Math.max(...arr) أو push(...arr) على مصفوفة ضخمة؟ حلقة أو reduce.
  • عمق كبير مشروع؟ حلقة بمكدس يدوي.

الخلاصة

Maximum call stack size exceeded تعني أن الاستدعاءات تراكمت أكثر مما يتسع له المكدس، وغالبًا لأن دالة تستدعي نفسها بلا باب خروج. اقرأ الاسم المتكرر في الـ trace، وأصلح شرط التوقف، أو حوّل العمق الكبير إلى حلقة.

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

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

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

ما معنى Maximum call stack size exceeded؟

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

كم استدعاءً يحتمل المكدس؟

يختلف حسب المحرك وحجم كل إطار. قسنا دالة فارغة تستدعي نفسها: حوالي 12,500 استدعاء في Node.js 22 وفي Chromium. الدوال ذات المتغيرات المحلية الكثيرة تصل الحد أسرع. لا تبنِ كودك على رقم محدد: إن احتجت آلاف المستويات فحوّله لحلقة.

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

نعم في حالتين: نشر مصفوفة ضخمة كوسائط (قسنا Math.max(...arr): نجح مع 120 ألف عنصر وفشل مع 130 ألفًا في Node 22)، أو دالة تمر على كائن فيه مرجع دائري لنفسه (نسخ عميق يدوي مثلًا). الحل حلقة أو reduce للأولى، وstructuredClone أو تتبّع العناصر المزورة للثانية.

ما الخطأ المقابل في Firefox وبايثون؟

في Firefox يظهر InternalError: too much recursion، وفي Safari RangeError: Maximum call stack size exceeded. وفي بايثون RecursionError: maximum recursion depth exceeded، والحد الافتراضي 1000 استدعاء (sys.getrecursionlimit()). وفي Java StackOverflowError. والسبب والحل واحد في الجميع.

اقرأ أيضًا

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