Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

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.

Rodolfo Mori7 min de leitura

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:

js
console.log('antes :', totalDoPedido);

var totalDoPedido = 479.7;

console.log('depois:', totalDoPedido);
antes : undefined depois: 479.7

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:

js
console.log(formatarPreco(289.9));

function formatarPreco(valor) {
  return `R$ ${valor.toFixed(2)}`;
}
R$ 289.90

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.

js
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]);
file:///private/tmp/loja/hoist-let.mjs:2 console.log('conferindo o total:', total); ^

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:

js
const teclado = new Produto('Teclado mecânico', 289.9);

class Produto {
  constructor(nome, preco) {
    this.nome = nome;
    this.preco = preco;
  }
}

console.log(teclado);
file:///private/tmp/loja/hoist-class.mjs:1 const teclado = new Produto('Teclado mecânico', 289.9); ^

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:

js
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';
declaration : function var : undefined nunca criada: undefined file:///private/tmp/loja/hoist-typeof.mjs:4 console.log('let na TDZ :', typeof cupom); ^

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:

js
function orcar(aPagar = subtotal + frete, subtotal = 289.9, frete = 24.9) {
  return aPagar;
}

console.log(orcar());
file:///private/tmp/loja/hoist-parametro.mjs:1 function orcar(aPagar = subtotal + frete, subtotal = 289.9, frete = 24.9) { ^

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:

js
if (true) {
  function emitirNota() {
    return 'nota fiscal emitida';
  }
}

console.log(emitirNota());
file:///private/tmp/loja/hoist-funcao-bloco.mjs:7 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.

var dentro de bloco continua vazando, e o resultado costuma parecer correto:

js
function definirStatus(pedido) {
  if (pedido.pago) {
    var status = 'pago';
  } else {
    var status = 'pendente';
  }
  return status;
}

console.log(definirStatus({ pago: true }), definirStatus({ pago: false }));
pago pendente

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.

js
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));
457.35

Quatro regras que produzem esse resultado:

  • const por padrão, let quando muda, var nunca. 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-define e no-var pegam 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.

  • hoisting
  • tdz
  • var
  • let
  • class

Perguntas frequentes

O motor realmente move o código para cima?
Não move nada. Antes de executar um escopo, o motor percorre as declarações e registra os nomes com o estado inicial de cada tipo. Hoisting é o apelido desse registro prévio. A metáfora do içamento explica var, mas confunde na TDZ — ali o nome está registrado e mesmo assim é inacessível.
Se let e const também sofrem hoisting, qual a vantagem sobre var?
A vantagem é o estado. Com var o nome já vale undefined e o programa segue com um valor errado até quebrar em outro lugar. Com let o acesso antecipado lança na hora, na linha do erro. A TDZ é um recurso de diagnóstico, não um incômodo.
Por que typeof em uma variável na TDZ dá erro?
Porque typeof só é seguro para nomes que não existem em escopo nenhum. Uma variável em TDZ existe, está registrada e o acesso é bloqueado — inclusive pelo typeof. É a única situação em que typeof pode lançar exceção.
Posso confiar em hoisting para organizar meu arquivo?
Para function declaration, sim, e isso é comum: as funções auxiliares no fim, o fluxo principal no topo. Para variáveis, não. Escrever código que depende de var valendo undefined antes da atribuição é depender de um acidente histórico da linguagem.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. MDN — Hoisting — developer.mozilla.org
  2. MDN — let e a zona morta temporal — developer.mozilla.org
  3. ECMAScript 2026 — Declarations and the Variable Statement — tc39.es

Continue por aqui