تشغّل الكود فيتوقف محرك جافاسكريبت برسالة قصيرة:
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 = obj | structuredClone أو تتبّع ما زرته |
| نشر مصفوفة ضخمة | 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 وEdge | RangeError: Maximum call stack size exceeded |
| Firefox | InternalError: too much recursion |
| Python | RecursionError: maximum recursion depth exceeded (الحد الافتراضي 1000) |
| Java | java.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.