Diego Betto's Blog
Photo by Tine Ivanič on Unsplash

Diego Betto · 11 settembre 2026 · 5 min di lettura

"Maximum call stack size exceeded": quando la ricorsione va fuori controllo

Cos'è davvero il call stack, le due cause più comuni di uno stack overflow in JavaScript, e come convertire una ricorsione profonda in versione iterativa.

Condividi:XLinkedInFacebookWhatsApp

La tua funzione gira bene su input piccoli, poi questo compare nel momento in cui ci lanci dati veri:

⛔ Errore

RangeError: Maximum call stack size exceeded

A differenza della maggior parte degli errori JavaScript, questo non riguarda davvero un valore sbagliato — riguarda quante chiamate di funzione sono impilate una sull’altra in un dato momento. Capire il call stack spiega sia le cause sia la correzione.

Cos’è davvero il call stack

Ogni volta che una funzione ne chiama un’altra, il motore inserisce un nuovo “frame” nel call stack, tenendo traccia di dove riprendere una volta che quella chiamata ritorna. Lo stack ha un limite di dimensione fisso (varia per motore e memoria disponibile, ma è finito). Quando lo superi, ottieni questo errore invece che il processo esaurisca silenziosamente la memoria.

function countDown(n) {
  console.log(n);
  return countDown(n - 1); // nessun caso base — non si ferma mai
}

countDown(5); // RangeError: Maximum call stack size exceeded

Ogni chiamata a countDown resta sullo stack in attesa che quella chiamata ritorni — e siccome non succede mai, lo stack cresce finché non raggiunge il limite.

Causa #1: ricorsione senza un caso base corretto

L’esempio sopra è la versione da manuale, ma di solito nel codice reale è più sottile — un errore di uno nella condizione di terminazione, o un caso base tecnicamente presente ma mai effettivamente raggiunto per certi input:

function factorial(n) {
  if (n === 0) return 1;
  return n * factorial(n - 1);
}

factorial(-5); // n non raggiunge mai 0, decrementa all'infinito — stesso errore

La correzione è assicurarsi che il caso base sia raggiungibile per ogni input valido, e validare gli input che ovviamente non lo raggiungerebbero mai:

function factorial(n) {
  if (n < 0) throw new RangeError("factorial non è definito per numeri negativi");
  if (n === 0) return 1;
  return n * factorial(n - 1);
}

Causa #2: getter/setter che fanno riferimento a se stessi

Meno ovvio, ma una fonte comune di stack overflow confusi:

class Product {
  get price() {
    return this.price; // legge il getter dentro cui è definito — ricorsione infinita
  }
}

Sembra un refuso una volta che lo individui, ma è facile da introdurre quando si trasforma una proprietà semplice in una calcolata dimenticandosi di rinominare il campo di supporto:

class Product {
  #price;

  get price() {
    return this.#price; // legge invece il campo privato di supporto
  }

  set price(value) {
    this.#price = value;
  }
}

Convertire una ricorsione profonda in versione iterativa

Per una ricorsione legittimamente profonda (attraversare un albero grande, elaborare una lista concatenata lunga) dove non c’è un bug, solo più profondità di quanta lo stack ne permetta, la correzione è strutturale: sostituire le chiamate ricorsive con un loop esplicito e uno stack tuo (un array), invece di affidarsi al call stack.

// Ricorsivo — può andare in overflow su una lista abbastanza lunga
function sumList(node) {
  if (!node) return 0;
  return node.value + sumList(node.next);
}

// Iterativo — profondità dello stack costante indipendentemente dalla lunghezza della lista
function sumList(node) {
  let sum = 0;
  while (node) {
    sum += node.value;
    node = node.next;
  }
  return sum;
}

Per l’attraversamento di un albero, il trucco equivalente è un array esplicito che funge da stack, sostituendo il call stack implicito:

function sumTree(root) {
  let sum = 0;
  const stack = [root];

  while (stack.length > 0) {
    const node = stack.pop();
    if (!node) continue;
    sum += node.value;
    stack.push(node.left, node.right);
  }

  return sum;
}

💡 Consiglio

JavaScript non ha l’ottimizzazione delle tail-call garantita nella maggior parte dei motori (fa parte della spec, ma V8 — il motore dietro Chrome e Node — non l’ha mai implementata), quindi non affidarti a “basta renderla tail-recursive” come correzione nel modo in cui potresti fare in altri linguaggi. La riscrittura iterativa sopra è l’opzione affidabile.

È sempre un bug, o può essere un vero limite di memoria?

Esistono entrambi i casi, e vale la pena distinguerli. Un vero bug logico (caso base mancante/irraggiungibile) fallisce a una profondità piccola e costante ogni volta. Una ricorsione legittimamente profonda su input insolitamente grande (un enorme albero JSON, una lista concatenata molto lunga) può fallire solo occasionalmente, alla profondità che lo stack attuale permette — non è un bug nella tua ricorsione, è un disallineamento strutturale tra il tuo algoritmo e quanta profondità è effettivamente disponibile, e la riscrittura iterativa sopra è la correzione giusta in entrambi i casi.

FAQ

❓ È la stessa cosa di un memory leak?

No — un memory leak è una crescita illimitata di oggetti trattenuti nel tempo, di solito attraverso molte operazioni, e non lancia questo errore specifico. Uno stack overflow è una singola catena di chiamate che diventa troppo profonda in un’unica sequenza sincrona. Entrambi possono derivare da ricorsione fuori controllo, ma sono modalità di fallimento diverse con correzioni diverse.

❓ Posso aumentare la dimensione dello stack invece di correggere la ricorsione?

In Node.js, sì, con il flag --stack-size — ma alza solo il tetto, non corregge un vero bug di ricorsione infinita, e non è qualcosa che puoi fare affatto in un ambiente browser. Trattalo come ultima risorsa per ricorsione genuinamente profonda (ma finita), non come sostituto della ricerca di un caso base mancante.

❓ async/await evita questo problema?

Non direttamente — la ricorsione dentro una funzione async può comunque andare in overflow dello stack allo stesso modo, dato che ogni await non resetta di per sé la profondità dello stack. Ciò che cambia è che ogni tick di microtask ottiene di fatto uno stack fresco, quindi una ricorsione che cede il controllo tramite await tra una chiamata e l’altra (invece di richiamarsi sincronamente) aggira il problema come effetto collaterale, non perché async corregga direttamente la profondità di ricorsione.

Conclusione

“Maximum call stack size exceeded” significa sempre che troppe chiamate di funzione sono state impilate sincronamente — o per un caso base mancante o irraggiungibile, un getter/setter che fa riferimento a se stesso, o una ricorsione strutturalmente corretta ma più profonda di quanto lo stack permetta per un input grande. Le prime due sono bug da correggere direttamente; la terza richiede di riscrivere le chiamate ricorsive come un loop esplicito con uno stack tuo.

Riferimenti

Condividi:XLinkedInFacebookWhatsApp
Diego Betto

Scritto da

Diego Betto

Co-Fondatore & CTO presso PAPION. Senior full-stack engineer specializzato in React, TypeScript, Node.js e sicurezza applicativa.