$cd~/artigos/call-stack-pilha-execucao-javascript
Fundamentals
série 33 Conceitos JS·parte 10 de 38

Call Stack: A Pilha que Organiza a Execução do JavaScript

10 minVanderlei José Alamino

Como o JavaScript sabe qual função está rodando no momento? Quando uma função chama outra, como o JavaScript sabe para onde voltar quando ela termina?

snippetJavaScript
function greet(name) {  const message = createMessage(name);  console.log(message);} function createMessage(name) {  return "Hello, " + name + "!";} greet("Alice");  // "Hello, Alice!"

Quando greet chama createMessage, o JavaScript precisa lembrar exatamente onde estava dentro de greet para poder retomar o trabalho depois que createMessage terminar. O mecanismo que torna isso possível é a call stack — a pilha de chamadas.

Neste artigo, vamos desmontar como essa pilha funciona, por que o JavaScript precisa dela, e como evitar o temido erro de stack overflow.

call-stack-pilha-execucao-javascript.meta.jsonjson

// metadados do artigo

{
  "tag": "fundamentos",
  "readTime": "10 min",
  "author": "Vanderlei José Alamino",
  "source": "33jsconcepts.com",
  "series": "33 Conceitos JS",
  "part": {
    "current": 10,
    "total": 38
  }
}
abrir fonte original
call-stack-pilha-execucao-javascript.mdmarkdown

Fonte de referência

Conteúdo adaptado a partir do guia original em inglês.

33jsconcepts.com
01

seção

A Pilha de Pratos: Uma Analogia do Mundo Real

Imagine que você está na cozinha de um restaurante, lavando pratos. Conforme os pratos limpos saem, você os empilha um sobre o outro. Quando um garçom precisa de um prato, ele sempre pega o do topo da pilha — nunca do meio ou do fundo.

snippettext
        ┌───────────┐        │  Prato 3  │  ← Você adiciona aqui (topo)        ├───────────┤        │  Prato 2  │        ├───────────┤        │  Prato 1  │  ← Primeiro prato (base)        └───────────┘

É exatamente assim que o JavaScript rastreia suas funções! Quando você chama uma função, o JavaScript a coloca no topo de uma "pilha". Quando essa função termina, o JavaScript a remove do topo e volta para o que estava embaixo.

Esse conceito simples — adicionar no topo e remover do topo — é a base de como o JavaScript executa seu código.

02

seção

O Que é a Call Stack?

A call stack (pilha de chamadas) é o mecanismo que o JavaScript usa para rastrear onde está no seu código. Pense nela como a "lista de tarefas" do JavaScript para chamadas de função — mas uma lista em que só pode trabalhar no item do topo.

snippetJavaScript
function first() { second(); }function second() { third(); }function third() { console.log('Hello!'); } first();// Pilha cresce: [first] → [second, first] → [third, second, first]// Pilha encolhe: [second, first] → [first] → []

O Princípio LIFO

A call stack segue o princípio LIFO: Last In, First Out (Último a Entrar, Primeiro a Sair).

snippettext
LIFO = Last In, First Out (Último a Entrar, Primeiro a Sair)┌─────────────────┐│   function C    │  ← Última a entrar (chamada mais recente)├─────────────────┤     Primeira a terminar e sair│   function B    │├─────────────────┤│   function A    │  ← Primeira a entrar (chamada mais antiga)└─────────────────┘     Última a terminar
  • Last In — A chamada de função mais recente vai para o topo
  • First Out — A função do topo deve terminar antes que possamos acessar as de baixo

Por Que o JavaScript Precisa de uma Call Stack?

O JavaScript é single-threaded (monothread): só pode fazer uma coisa por vez. Segundo a especificação ECMAScript, cada invocação de função cria um novo contexto de execução que é empilhado na pilha. A call stack ajuda o JavaScript a:

  1. Lembrar onde está — Qual função está executando agora?
  1. Saber para onde voltar — Quando uma função termina, onde a execução deve continuar?
  1. Rastrear variáveis locais — Cada função tem suas próprias variáveis que não devem interferir nas outras

Especificação ECMAScript: Segundo a especificação oficial do JavaScript, a call stack é implementada através de "contextos de execução." Cada chamada de função cria um novo contexto de execução que é empilhado.

03

seção

Como a Call Stack Funciona: Passo a Passo

Vamos rastrear um exemplo simples para ver a call stack em ação.

Um Exemplo Simples

snippetJavaScript
function greet(name) {  const greeting = createGreeting(name);  console.log(greeting);} function createGreeting(name) {  return "Hello, " + name + "!";} // Comece aquigreet("Alice");console.log("Done!");

Execução Passo a Passo

O Programa Começa

O JavaScript começa a executar seu código de cima para baixo. A call stack está vazia.

snippettext
Call Stack: [ vazia ]

greet('Alice') é Chamada

O JavaScript vê greet("Alice") e empilha greet na call stack.

snippettext
Call Stack: [ greet ]

Agora o JavaScript entra na função greet e começa a executar seu código.

createGreeting('Alice') é Chamada

Dentro de greet, o JavaScript encontra createGreeting(name). Ele empilha createGreeting na pilha.

snippettext
Call Stack: [ createGreeting, greet ]

Observe: greet fica pausada enquanto createGreeting executa. O JavaScript só pode fazer uma coisa por vez!

createGreeting Retorna

createGreeting termina e retorna "Hello, Alice!". O JavaScript a desempilha.

snippettext
Call Stack: [ greet ]

O valor de retorno ("Hello, Alice!") é passado de volta para greet.

greet Continua e Termina

De volta em greet, o valor retornado é armazenado em greeting, depois console.log executa. Por fim, greet termina e é desempilhada.

snippettext
Call Stack: [ vazia ]

O Programa Continua

Com a pilha vazia, o JavaScript continua na próxima linha: console.log("Done!").

Saída:

snippettext
Hello, Alice!Done!

Resumo Visual

snippettext
Passo 1:          Passo 2:          Passo 3:              Passo 4:          Passo 5:┌─────────┐      ┌─────────┐      ┌────────────────┐   ┌─────────┐      ┌─────────┐│ (vazia) │  →   │  greet  │  →   │createGreeting  │ → │  greet  │  →   │ (vazia) │└─────────┘      └─────────┘      ├────────────────┤   └─────────┘      └─────────┘                                  │     greet      │                                  └────────────────┘Programa          greet()          createGreeting()     createGreeting   greet()inicia            chamada          chamada              retorna          retorna

Tabela de Execução

PassoAçãoPilha (topo → base)O que está acontecendo
1Início[]Programa começa
2Chama greet("Alice")[greet]Entra na função greet
3Chama createGreeting("Alice")[createGreeting, greet]greet pausa, entra createGreeting
4Retorno de createGreeting[greet]createGreeting termina, volta para greet
5Retorno de greet[]greet termina, programa continua
6console.log("Done!")[]Imprime "Done!"
04

seção

Contexto de Execução: O Que Realmente Está na Pilha?

Quando dizemos que uma função está "na pilha", o que isso significa de verdade? Cada entrada na call stack é um contexto de execução — às vezes chamado de stack frame (frame de pilha) em ciência da computação. Ele contém tudo que o JavaScript precisa para executar aquela função.

Argumentos da Função

Os valores passados para a função quando ela foi chamada.

snippetJavaScript
function greet(name, age) {  // Argumentos: { name: "Alice", age: 25 }}greet("Alice", 25);

Variáveis Locais

Variáveis declaradas dentro da função com var, let ou const.

snippetJavaScript
function calculate() {  const x = 10;      // Variável local  let y = 20;        // Variável local  var z = 30;        // Variável local  // Estas só existem dentro desta função}

A Palavra-chave this

O valor de this dentro da função, que depende de como a função foi chamada.

snippetJavaScript
const person = {  name: "Alice",  greet() {    console.log(this.name); // 'this' refere-se a person  }};

Endereço de Retorno

Onde o JavaScript deve continuar executando depois que esta função retornar. É assim que o JavaScript sabe voltar ao lugar certo no seu código.

Cadeia de Escopo (Scope Chain)

Acesso a variáveis de funções externas (pais). É assim que closures funcionam!

snippetJavaScript
function outer() {  const message = "Hello";    function inner() {    console.log(message); // Pode acessar 'message' de outer  }    inner();}

Visualizando um Contexto de Execução

snippettext
┌─────────────────────────────────────────┐│         CONTEXTO DE EXECUÇÃO            ││         Função: greet                   │├─────────────────────────────────────────┤│  Argumentos:    { name: "Alice" }       ││  Variáveis:     { greeting: undefined } ││  this:          window (ou undefined)     ││  Retornar para: linha 12, programa      ││  Escopo externo: [escopo global]        │└─────────────────────────────────────────┘
05

seção

Chamadas de Função Aninhadas: Um Exemplo Mais Profundo

Vamos olhar um exemplo mais complexo com vários níveis de chamadas de função.

snippetJavaScript
function multiply(x, y) {  return x * y;} function square(n) {  return multiply(n, n);} function printSquare(n) {  const result = square(n);  console.log(result);} printSquare(4);

Rastreando a Execução

Passo a Passo

Passo 1: Chama printSquare(4)

snippettext
Pilha: [ printSquare ]

Passo 2: printSquare chama square(4)

snippettext
Pilha: [ square, printSquare ]

Passo 3: square chama multiply(4, 4)

snippettext
Pilha: [ multiply, square, printSquare ]

Esta é a profundidade máxima da pilha neste programa: 3 frames.

Passo 4: multiply retorna 16

snippettext
Pilha: [ square, printSquare ]

Passo 5: square retorna 16

snippettext
Pilha: [ printSquare ]

Passo 6: printSquare imprime 16 e retorna

snippettext
Pilha: [ vazia ]

Saída: 16

Diagrama Completo

snippettext
printSquare(4)     square(4)          multiply(4,4)       multiply          square           printSquarechamada            chamada            chamada             retorna 16        retorna 16       retorna┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐   ┌─────────────┐   ┌─────────┐│printSquare  │ →  │   square    │ →  │  multiply   │ →  │   square    │ → │printSquare  │ → │ (vazia) │└─────────────┘    ├─────────────┤    ├─────────────┤    ├─────────────┤   └─────────────┘   └─────────┘                   │printSquare  │    │   square    │    │printSquare  │                   └─────────────┘    ├─────────────┤    └─────────────┘                                      │printSquare  │                                      └─────────────┘Profundidade: 1     Profundidade: 2     Profundidade: 3     Profundidade: 2   Profundidade: 1   Profundidade: 0

Entendendo o fluxo: cada função precisa terminar completamente antes de a função que a chamou poder continuar. Por isso printSquare precisa esperar square, e square precisa esperar multiply.

06

seção

O Erro #1 da Call Stack: Stack Overflow

A call stack tem tamanho limitado. O limite padrão varia conforme o engine — o V8 do Chrome costuma permitir cerca de 10.000–15.000 frames, enquanto o SpiderMonkey do Firefox tem limite similar. Se você continuar empilhando funções sem removê-las, eventualmente ficará sem espaço. Isso é chamado de stack overflow (estouro de pilha), e o JavaScript lança um RangeError quando isso acontece.

snippettext
┌─────────────────────────────────────────────────────────────────────────┐│                         STACK OVERFLOW                                   │├─────────────────────────────────────────────────────────────────────────┤│                                                                          ││  ERRADO: Sem Caso Base                    CERTO: Com Caso Base           ││  ────────────────────                     ─────────────────────            ││                                                                          ││  function count() {                     function count(n) {              ││    count()  // Para sempre!               if (n <= 0) return  // Pare!   ││  }                                        count(n - 1)                   ││                                         }                                ││                                                                          ││  Pilha cresce para sempre...            Pilha cresce, depois encolhe     ││  ┌─────────┐                            ┌─────────┐                      ││  │ count() │                            │ count(0)│ ← Retorna            ││  ├─────────┤                            ├─────────┤                      ││  │ count() │                            │ count(1)│                      ││  ├─────────┤                            ├─────────┤                      ││  │ count() │                            │ count(2)│                      ││  ├─────────┤                            └─────────┘                      ││  │  ....   │                                                             ││  └─────────┘                                                             ││  💥 CRASH!                              ✓ Sucesso!                       ││                                                                          │└─────────────────────────────────────────────────────────────────────────┘

A armadilha: funções recursivas sem condição de parada adequada vão travar seu programa. A causa mais comum é recursão infinita — uma função que chama a si mesma para sempre sem caso base.

O Erro Clássico: Caso Base Ausente

snippetJavaScript
// ❌ RUIM: Isso vai travar!function countdown(n) {  console.log(n);  countdown(n - 1);  // Chama a si mesma para sempre!} countdown(5);

O que acontece:

snippettext
Pilha: [ countdown(5) ]Pilha: [ countdown(4), countdown(5) ]Pilha: [ countdown(3), countdown(4), countdown(5) ]Pilha: [ countdown(2), countdown(3), countdown(4), countdown(5) ]... continua crescendo para sempre ...💥 CRASH: Maximum call stack size exceeded

A Correção: Adicionar um Caso Base

snippetJavaScript
// ✅ BOM: Isso funciona corretamentefunction countdown(n) {  if (n <= 0) {    console.log("Done!");    return;  // ← CASO BASE: Pare aqui!  }  console.log(n);  countdown(n - 1);} countdown(5);// Saída: 5, 4, 3, 2, 1, Done!

O que acontece agora:

snippettext
Pilha: [ countdown(5) ]Pilha: [ countdown(4), countdown(5) ]Pilha: [ countdown(3), countdown(4), countdown(5) ]Pilha: [ countdown(2), countdown(3), ..., countdown(5) ]Pilha: [ countdown(1), countdown(2), ..., countdown(5) ]Pilha: [ countdown(0), countdown(1), ..., countdown(5) ]       ↑ Caso base alcançado! Começar a retornar.Pilha: [ countdown(1), ..., countdown(5) ]Pilha: [ countdown(2), ..., countdown(5) ]... pilha desenrola ...Pilha: [ countdown(5) ]Pilha: [ vazia ]✅ Programa completa com sucesso

Mensagens de Erro por Navegador

NavegadorMensagem de Erro
ChromeRangeError: Maximum call stack size exceeded
FirefoxInternalError: too much recursion (não padrão)
SafariRangeError: Maximum call stack size exceeded

O Firefox usa InternalError, que é um tipo de erro não padrão específico do engine SpiderMonkey. Chrome e Safari usam o RangeError padrão.

Causas Comuns de Stack Overflow

1. Recursão Infinita (Sem Caso Base)

snippetJavaScript
function loop() {  loop();}loop(); // 💥 Crash!

2. Caso Base Nunca Alcançado

snippetJavaScript
function countUp(n) {  if (n >= 1000000000000) return; // Muito longe!  countUp(n + 1);}countUp(0); // 💥 Crash antes de alcançar o caso base

3. Recursão Acidental em Setters

snippetJavaScript
class Person {  set name(value) {    this.name = value; // Chama o setter de novo! Loop infinito!  }} const p = new Person();p.name = "Alice"; // 💥 Crash! // Correção: use outro nome de propriedadeclass PersonFixed {  set name(value) {    this._name = value;  }}

4. Chamadas Circulares Entre Funções

snippetJavaScript
function a() { b(); }function b() { a(); } // a chama b, b chama a, para sempre! a(); // 💥 Crash!

Dicas de prevenção:

  1. Sempre defina um caso base claro para funções recursivas
  1. Garanta que cada chamada recursiva se aproxima do caso base
  1. Considere usar iteração (loops) em vez de recursão para casos simples
  1. Cuidado com setters de propriedade — use nomes internos diferentes
07

seção

Debugando a Call Stack

Quando algo dá errado, a call stack é sua melhor amiga para descobrir o que aconteceu.

Lendo um Stack Trace

Quando ocorre um erro, o JavaScript fornece um stack trace — um instantâneo da call stack no momento do erro.

snippetJavaScript
function a() { b(); }function b() { c(); }function c() {   throw new Error('Algo deu errado!'); } a();

Saída:

snippettext
Error: Algo deu errado!    at c (script.js:4:9)    at b (script.js:2:14)    at a (script.js:1:14)    at script.js:7:1

Como ler:

  • Leia de cima para baixo = da chamada mais recente à mais antiga
  • at c (script.js:4:9) = o erro ocorreu na função c, arquivo script.js, linha 4, coluna 9
  • O trace mostra exatamente como o programa chegou ao erro

Usando as DevTools do Navegador

Abra as DevTools

Pressione F12 ou Cmd+Option+I (Mac) / Ctrl+Shift+I (Windows)

Vá para a Aba Sources

Clique na aba "Sources" (Chrome) ou "Debugger" (Firefox)

Defina um Breakpoint

Clique no número de uma linha do seu código para definir um breakpoint. A execução pausa ali.

Veja a Call Stack

Quando pausado, olhe o painel "Call Stack" à direita. Ele mostra todas as funções atualmente na pilha.

Percorra o Código

Use os botões de passo para executar linha a linha e observe a pilha mudar.

Dica pro de debug: Se você está lidando com recursão, adicione um console.log no início da sua função para ver quantas vezes ela está sendo chamada:

snippetJavaScript
function factorial(n) {  console.log('factorial chamada com n =', n);  if (n <= 1) return 1;  return n * factorial(n - 1);}
08

seção

A Call Stack e Código Assíncrono

Você pode estar se perguntando: "Se o JavaScript só pode fazer uma coisa por vez, como lida com setTimeout ou buscar dados de um servidor?"

Ótima pergunta! A call stack é só parte do quadro.

Quando você usa funções assíncronas como setTimeout, fetch ou event listeners, o JavaScript não as coloca na call stack imediatamente. Em vez disso, elas passam por um sistema diferente envolvendo o Event Loop e a Task Queue.

Isso é coberto em detalhes na seção de Event Loop. Mas aqui vai um spoiler:

snippetJavaScript
console.log('First'); setTimeout(() => {  console.log('Second');}, 0); console.log('Third'); // Saída:// First// Third// Second  ← Mesmo com delay de 0ms, isso roda por último!

O callback do setTimeout não vai diretamente para a call stack. Ele espera em uma fila até a pilha estar vazia. Como Philip Roberts demonstrou em sua aclamada palestra do JSConf EU "What the heck is the event loop?" (vista mais de 8 milhões de vezes), é por isso que "Third" imprime antes de "Second" mesmo que o timeout seja de 0 milissegundos.

09

seção

Conceitos Errados Comuns

"A call stack e o heap de memória são a mesma coisa"

Errado! A call stack e o heap são estruturas completamente diferentes:

ComponentePropósitoEstrutura
Call StackRastreia execução de funçõesOrdenada (LIFO), pequena, rápida
HeapArmazena dados (objetos, arrays)Não estruturado, grande
snippetJavaScript
function example() {  // Primitivos vivem no frame da pilha  const x = 10;  const name = "Alice";    // Objetos vivem no HEAP (referência armazenada na pilha)  const user = { name: "Alice" };  const numbers = [1, 2, 3];}

Quando a função retorna, o frame é desempilhado (primitivos somem), mas objetos no heap persistem até o garbage collector.

"Callbacks assíncronos executam imediatamente quando o timer termina"

Errado! Quando um timer termina, o callback NÃO roda imediatamente. Ele vai para a Task Queue e deve esperar:

  1. A call stack estar completamente vazia
  1. Todas as microtasks serem processadas primeiro
  1. Sua vez na fila de tarefas
snippetJavaScript
console.log('Start'); setTimeout(() => {  console.log('Timer');  // NÃO roda em 0ms!}, 0); console.log('End'); // Saída: Start, End, Timer// Mesmo com delay de 0ms, 'Timer' imprime POR ÚLTIMO

"O JavaScript pode rodar múltiplas funções ao mesmo tempo"

Errado! O JavaScript é single-threaded. Ele tem UMA call stack e só consegue executar UMA coisa de cada vez.

snippetJavaScript
function a() {  console.log('A start');  b();  // JS pausa 'a' e roda 'b' completamente  console.log('A end');} function b() {  console.log('B');} a();// Saída: A start, B, A end (sequencial, não paralelo)

A fonte da confusão: As pessoas confundem o _comportamento assíncrono_ do JavaScript com _execução paralela_. Web APIs (timers, fetch, etc.) rodam em threads separadas do navegador, mas o código JavaScript em si roda uma operação de cada vez. O Event Loop coordena callbacks, criando a _ilusão_ de concorrência.

"Promises são completamente assíncronas"

Errado! O _construtor_ da Promise roda sincronamente. Apenas os callbacks .then() são assíncronos:

snippetJavaScript
console.log('1'); new Promise((resolve) => {  console.log('2');  // Roda SINCRONAMENTE!  resolve();}).then(() => {  console.log('3');  // Assíncrono (microtask)}); console.log('4'); // Saída: 1, 2, 4, 3// Note: '2' imprime antes de '4'!

A função executor passada para new Promise() roda imediatamente na call stack. Apenas os callbacks .then(), .catch() e .finally() são enfileirados como microtasks.

10

seção

Teste Seu Conhecimento

$ summary --quick

Resumo rápido

  • O JavaScript é single-threaded — Ele tem UMA call stack e só consegue fazer uma coisa de cada vez
  • Princípio LIFO — Last In, First Out. A chamada de função mais recente termina primeiro
  • Contextos de execução — Cada chamada de função cria um "frame" contendo argumentos, variáveis locais e endereço de retorno
  • Execução síncrona — Funções devem completar antes que suas chamadoras possam continuar
  • Stack overflow — Acontece quando a pilha fica muito profunda, geralmente por recursão infinita
  • Sempre tenha um caso base — Funções recursivas precisam de uma condição de parada
  • Stack traces são seus amigos — Eles mostram exatamente como seu programa chegou a um erro
  • Callbacks assíncronos esperam — setTimeout, fetch e callbacks de evento não rodam até a call stack estar vazia
  • Cada frame é isolado — Variáveis locais em uma chamada de função não afetam variáveis em outra chamada da mesma função
  • Ferramentas de debug mostram a pilha — As DevTools do navegador permitem pausar a execução e inspecionar a call stack atual

Créditos da série

Este artigo faz parte de uma série de estudos inspirada no projeto 33 Conceitos JS, criado por Leonardo Maldonado. Todo o conteúdo original está disponível em 33jsconcepts.com.