Hoisting em JavaScript: o que é e como evitar erro
var vira undefined, function sobe inteira, let e class caem na zona morta temporal. Os quatro comportamentos de hoisting com o erro real de cada um.
Hoisting é o registro das declarações de um escopo antes de a primeira linha
dele rodar. A parte que confunde: os quatro tipos de declaração são
registrados em estados diferentes. var nasce valendo undefined,
function nasce completa, let e const nascem inacessíveis, class também.
Na prática isso significa que chamar algo antes da linha em que ele aparece pode
funcionar, devolver undefined em silêncio ou lançar exceção — dependendo só da
palavra que você usou para declarar. Os quatro casos aparecem aqui reproduzidos
no Node 24.16.0.
Se a lição de escopo em JavaScript mostrou onde um nome existe, esta mostra desde quando.
“Subir” é apenas a imagem popular. O comportamento técnico é a instanciação do escopo: antes de executar as instruções, o motor cria registros para os nomes declarados e dá a cada tipo de declaração um estado inicial diferente.
O mapa de nomes montado antes da primeira linha
Pense na lista de convidados preparada antes de um evento. Alguns nomes já
chegam com acesso liberado, outros constam como “aguardando credencial” e um
nome de var aparece com um campo vazio. Quando a execução começa, a lista já
existe; o que muda é o estado de cada registro. function, var, let,
const e class não entram nela do mesmo jeito.
Monte uma tabela antes dos exemplos com quatro linhas e três colunas: “nome registrado”, “valor antes da declaração” e “resultado do acesso”. Depois confira cada previsão no Node. A tabela substitui a frase imprecisa “o código foi para cima” por estados que você consegue observar.
Comece pelo caso que deu nome à ideia. Este arquivo não quebra:
console.log('antes :', totalDoPedido);
var totalDoPedido = 479.7;
console.log('depois:', totalDoPedido);O motor separou a linha 3 em duas partes. A declaração — “existe um nome
totalDoPedido neste escopo” — foi processada antes de tudo. A atribuição
continuou exatamente onde estava escrita. Entre uma coisa e outra, o nome existe
valendo undefined.
É esse undefined silencioso que faz de var uma má ideia em código novo. O
programa não avisa nada; ele imprime um valor errado e segue. Três funções
adiante, undefined vira NaN numa conta e você vai procurar o defeito no
lugar errado.
function: içada inteira
Function declaration é o único caso em que o valor sobe junto com o nome:
console.log(formatarPreco(289.9));
function formatarPreco(valor) {
return `R$ ${valor.toFixed(2)}`;
}Isso é intencional e útil: permite escrever o fluxo principal no topo do arquivo e deixar as funções auxiliares embaixo, na ordem em que fazem sentido para quem lê. É a única forma de hoisting em que vale a pena confiar de propósito.
O truque não vale para função guardada em variável. Ali quem é içado é a
variável, com o comportamento do var, let ou const que a criou — o assunto
da lição de
function expression contra declaration.
let e const: içadas, mas na zona morta temporal
let e const também são registrados antes de o escopo rodar. A diferença é
que o motor marca o nome como inacessível até a linha da declaração. Esse
intervalo é a zona morta temporal, ou TDZ.
function fecharPedido(precos) {
console.log('conferindo o total:', total);
const total = precos.reduce((soma, preco) => soma + preco, 0);
return total;
}
fecharPedido([289.9, 149.9]);ReferenceError: Cannot access ‘total’ before initialization at fecharPedido (file:///private/tmp/loja/hoist-let.mjs:2:38) at file:///private/tmp/loja/hoist-let.mjs:7:1 at ModuleJob.run (node:internal/modules/esm/module_job:439:25)
Node.js v24.16.0
A mensagem não é total is not defined. O motor sabe que total existe naquele
escopo — ele acabou de registrar o nome — e está dizendo que você chegou antes
da inicialização. Esse é o diagnóstico que var nunca te dá.
A TDZ vale por execução do escopo, não por posição no arquivo: cada chamada de
fecharPedido registra um total novo e o mantém inacessível até a linha 3
daquela execução. Chamar a função mais tarde não ajuda — ela quebra sempre, no
mesmo ponto. A correção é mover o console.log para depois da declaração.
class cai na mesma armadilha
Classe é declaração léxica, como let. Ela não é içada com o corpo:
const teclado = new Produto('Teclado mecânico', 289.9);
class Produto {
constructor(nome, preco) {
this.nome = nome;
this.preco = preco;
}
}
console.log(teclado);ReferenceError: Cannot access ‘Produto’ before initialization at file:///private/tmp/loja/hoist-class.mjs:1:17 at ModuleJob.run (node:internal/modules/esm/module_job:439:25)
Node.js v24.16.0
Muita gente escreve classe achando que ela se comporta como function, já que
antes de 2015 o construtor era literalmente uma função. Não se comporta:
class é açúcar sintático com semântica de let.
O único typeof que pode explodir
typeof é conhecido como o operador seguro — ele devolve a string
'undefined' para qualquer nome inexistente, sem lançar erro. A TDZ é a única
exceção:
console.log('declaration :', typeof calcularFrete);
console.log('var :', typeof aplicarCupom);
console.log('nunca criada:', typeof naoExisteEmLugarNenhum);
console.log('let na TDZ :', typeof cupom);
function calcularFrete() {}
var aplicarCupom = function () {};
let cupom = 'PRIMEIRACOMPRA';ReferenceError: Cannot access ‘cupom’ before initialization at file:///private/tmp/loja/hoist-typeof.mjs:4:9 at ModuleJob.run (node:internal/modules/esm/module_job:439:25)
Node.js v24.16.0
As três primeiras linhas resumem a lição inteira em uma execução só. A
declaration já é function; o var já é undefined; um nome que nunca foi
declarado também é undefined — e é por isso que typeof não serve para
distinguir “não existe” de “existe e vale undefined”. A quarta linha derruba o
programa.
Até o parâmetro padrão respeita a TDZ
A lista de parâmetros tem escopo próprio, avaliado da esquerda para a direita. Um padrão que dependa de um parâmetro declarado depois cai na mesma zona morta:
function orcar(aPagar = subtotal + frete, subtotal = 289.9, frete = 24.9) {
return aPagar;
}
console.log(orcar());ReferenceError: Cannot access ‘subtotal’ before initialization at orcar (file:///private/tmp/loja/hoist-parametro.mjs:1:25) at file:///private/tmp/loja/hoist-parametro.mjs:5:13 at ModuleJob.run (node:internal/modules/esm/module_job:439:25)
Node.js v24.16.0
O acento aponta para subtotal dentro da própria assinatura. Inverta a ordem —
function orcar(subtotal = 289.9, frete = 24.9, aPagar = subtotal + frete) — e
funciona. É o mesmo princípio da lição de
parâmetros e retorno: parâmetro padrão
é avaliado na chamada, não na definição.
Function declaration dentro de bloco
Um caso que pega muita gente na revisão de código antigo. Declaração de função dentro de chaves é, em modo estrito, escopo de bloco — o nome não existe do lado de fora:
if (true) {
function emitirNota() {
return 'nota fiscal emitida';
}
}
console.log(emitirNota());ReferenceError: emitirNota is not defined at file:///private/tmp/loja/hoist-funcao-bloco.mjs:7:9 at ModuleJob.run (node:internal/modules/esm/module_job:439:25)
Node.js v24.16.0
Em script clássico, fora do modo estrito, o mesmo código funcionaria — o nome
vazaria para a função inteira. É uma das poucas construções cujo comportamento
muda entre módulo e script, e por isso a recomendação é simples: nunca declare
função dentro de bloco. Se a escolha é condicional, use uma
function expression atribuída a um let
declarado fora.
Já var dentro de bloco continua vazando, e o resultado costuma parecer
correto:
function definirStatus(pedido) {
if (pedido.pago) {
var status = 'pago';
} else {
var status = 'pendente';
}
return status;
}
console.log(definirStatus({ pago: true }), definirStatus({ pago: false }));Duas declarações do mesmo nome na mesma função, sem nenhum erro. Trocar as duas
por let não dá conflito de nome — dá coisa pior: cada ramo do if ganha o seu
próprio status, os dois morrem no fecha-chaves, e o return quebra com
ReferenceError: status is not defined. A intenção real era uma variável com
dois valores possíveis, então o certo é let status; antes do if e a
atribuição dentro de cada ramo.
Como escrever código que não depende de hoisting
O objetivo não é decorar os quatro comportamentos; é escrever de um jeito em que eles nunca importem. O arquivo abaixo é imune: nada é usado antes de existir, e mudar a ordem das linhas não muda nada.
const TAXA_CARTAO = 0.0399;
function calcularSubtotal(itens) {
return itens.reduce((soma, item) => soma + item.preco, 0);
}
function calcularTotal(itens) {
const subtotal = calcularSubtotal(itens);
return Number((subtotal * (1 + TAXA_CARTAO)).toFixed(2));
}
const carrinho = [
{ nome: 'Teclado mecânico', preco: 289.9 },
{ nome: 'Mouse sem fio', preco: 149.9 },
];
console.log(calcularTotal(carrinho));Quatro regras que produzem esse resultado:
constpor padrão,letquando muda,varnunca. Metade dos problemas desta lição some com essa única decisão.- Declare perto do primeiro uso. Uma constante usada na linha 40 não precisa estar na linha 2.
- Deixe o ESLint reclamar. As regras
no-use-before-defineeno-varpegam esses casos antes do commit, e a segunda é praticamente consenso. - Confie no hoisting só das function declarations. Chamar no topo e definir embaixo é idiomático e legível.
| declaração | registrada antes? | estado inicial | acesso antes da linha |
|---|---|---|---|
function f() {} |
sim | a função completa | funciona |
var x |
sim | undefined |
devolve undefined |
let x / const x |
sim | inacessível (TDZ) | ReferenceError |
class X {} |
sim | inacessível (TDZ) | ReferenceError |
import |
sim | ligação já resolvida | funciona |
A próxima lição usa tudo isto de uma vez: quando uma função devolve outra, o
escopo em que ela nasceu continua vivo depois de a externa terminar. É
closure em JavaScript. Para revisar a
diferença entre var, let e const antes de seguir, volte a
variáveis em JavaScript; o
guia completo de JavaScript tem o mapa da trilha.
Perguntas frequentes
O motor realmente move o código para cima?
Se let e const também sofrem hoisting, qual a vantagem sobre var?
Por que typeof em uma variável na TDZ dá erro?
Posso confiar em hoisting para organizar meu arquivo?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — Hoisting — developer.mozilla.org
- MDN — let e a zona morta temporal — developer.mozilla.org
- ECMAScript 2026 — Declarations and the Variable Statement — tc39.es


