JavaScript: Pare de Usar var. Um Guia Visual de const e let
Por que o JavaScript moderno usa const e let em vez de var: reatribuição, escopo de bloco vs. de função, o erro dos callbacks no laço, hoisting e a zona morta temporal, redeclaração, execuções reais no Node.js, um teste de velocidade e exercícios com respostas.

Nesta página
Nos seus primeiros vinte anos, o JavaScript tinha um único jeito de declarar uma variável: var. Aí o ES6 (também chamado de ES2015) trouxe mais dois, const e let, e hoje a maioria dos guias de estilo resume o conselho em quatro palavras: pare de usar var.
Não é só moda. O var tem esquisitices que causam erros de verdade: variáveis que vazam para fora dos blocos, laços em que todos os callbacks enxergam o mesmo número e variáveis que dá para usar antes de declarar. Neste guia você vai ver cada esquisitice rodando de verdade, aprender a regra simples para escolher entre const e let e praticar como achar e consertar erros de var. Exercícios com respostas fecham o artigo.
Todas as saídas abaixo vêm de execuções reais com o Node.js 26. Elas são iguais em qualquer navegador moderno. As mensagens de erro do JavaScript são em inglês; vamos traduzindo as importantes.
A regra numa imagem

1// O jeito antigo
2var nome = "Angela";
3
4// O jeito novo
5const nome = "Angela";- Comece com
const. A maioria das variáveis recebe um valor uma vez e nunca é reatribuída. - Troque para
letsó quando precisar reatribuir a variável com=mais tarde. - Não use
varem código novo.
Vamos ver o porquê, um comportamento de cada vez.
const: atribuída uma vez, para sempre
Uma variável const recebe o seu valor na declaração e nunca pode ser reatribuída:
1const saudacao = "Olá";
2console.log(saudacao);
3
4try {
5 saudacao = "Tchau";
6} catch (erro) {
7 console.log(`${erro.name}: ${erro.message}`);
8}Olá
TypeError: Assignment to constant variable.("Atribuição a uma variável constante.") Pegamos o erro com try/catch para o programa poder imprimi-lo e continuar. Como nunca vai poder receber um valor depois, uma const precisa de um valor na hora. Este programa nem começa:
const resposta;SyntaxError: Missing initializer in const declaration("Falta o valor inicial na declaração const.") Essa é a graça do const: quando você lê um código e vê const total = …, sabe que total guarda aquele valor em tudo o que vem abaixo. Não precisa caçar uma linha que o muda.
O const tranca a etiqueta, não a caixa
Esta é a parte que surpreende. Você ainda pode mudar o que está dentro de um array ou objeto const:
1const pets = [];
2pets.push("gato");
3pets.push("cachorro");
4console.log(pets);
5
6const configuracoes = { tema: "claro" };
7configuracoes.tema = "escuro";
8configuracoes.tamanhoDaFonte = 16;
9console.log(configuracoes);
10
11try {
12 pets = ["hamster"];
13} catch (erro) {
14 console.log(`${erro.name}: ${erro.message}`);
15}[ 'gato', 'cachorro' ]
{ tema: 'escuro', tamanhoDaFonte: 16 }
TypeError: Assignment to constant variable.![O const tranca a etiqueta, não a caixa. A variável pets é uma etiqueta presa para sempre a um array. pets.push("gato") e pets.push("cachorro") podem: mudam o que está dentro do mesmo array, que agora guarda "gato" e "cachorro". pets = ["hamster"] não pode: tenta mudar a etiqueta para outro array, dando TypeError: Assignment to constant variable. const quer dizer "este nome sempre aponta para este valor", não "este valor nunca muda". Para trancar o conteúdo também, use Object.freeze, que é raso](https://nbkmundotech.us-southeast-1.linodeobjects.com/static/images/1b7d3298-e5b1-4a0b-831a-f33731e45605.png?v=1792875898102)
Uma variável é uma etiqueta colada num valor. O const cola a etiqueta num array para sempre, mas o array em si ainda pode mudar. Só o pets = …, que tenta mudar a etiqueta de lugar, é proibido.
Se você quiser trancar o conteúdo também, o Object.freeze faz isso, mas só um nível para dentro:
1const lugar = Object.freeze({ cidade: "Lisboa", etiquetas: ["mar"] });
2lugar.cidade = "Porto";
3lugar.etiquetas.push("sol");
4console.log(lugar);{ cidade: 'Lisboa', etiquetas: [ 'mar', 'sol' ] }A cidade não mudou (fora do modo estrito a atribuição é ignorada em silêncio; dentro dele, gera um erro), mas o array lá dentro ainda pôde crescer.
let: quando o valor precisa mudar
Use let quando a variável recebe valores novos de verdade com o tempo: contadores, totais, índices de laço ou um valor decidido por um if:
1let total = 0;
2for (const preco of [3.5, 4.25, 2]) {
3 total += preco;
4}
5console.log(total);
6
7let mensagem = "Bom dia";
8const hora = 15;
9if (hora >= 12) {
10 mensagem = "Boa tarde";
11}
12console.log(mensagem);9.75
Boa tardeRepare que o laço usa const preco: cada volta de um laço for…of ganha um preco novinho, então ele nunca é reatribuído. Um laço de contagem clássico é diferente, porque o indice++ reatribui a variável:
1const letras = ["a", "b", "c"];
2for (let indice = 0; indice < letras.length; indice++) {
3 console.log(indice, letras[indice]);
4}0 a
1 b
2 cEscopo: onde uma variável existe
Escopo é a região do código onde uma variável pode ser usada. É aqui que var e let/const mais diferem:
leteconsttêm escopo de bloco: existem só dentro do par de chaves{ }mais próximo, como o corpo de umifou de um laço.vartem escopo de função: ignora os blocos e pertence à função inteira em volta dele.

1function confereEscopos() {
2 if (true) {
3 var vazada = "var";
4 let contida = "let";
5 }
6 console.log(vazada);
7 try {
8 console.log(contida);
9 } catch (erro) {
10 console.log(`${erro.name}: ${erro.message}`);
11 }
12}
13
14confereEscopos();var
ReferenceError: contida is not defined("contida não está definida.") O mesmo acontece com as variáveis de laço. Um índice var sobrevive ao laço:
1for (var indiceAntigo = 0; indiceAntigo < 3; indiceAntigo++) {
2 console.log(indiceAntigo);
3}
4console.log("depois do laço com var:", indiceAntigo);
5
6for (let indiceNovo = 0; indiceNovo < 3; indiceNovo++) {
7 console.log(indiceNovo);
8}
9try {
10 console.log("depois do laço com let:", indiceNovo);
11} catch (erro) {
12 console.log(`${erro.name}: ${erro.message}`);
13}10
21
32
4depois do laço com var: 3
50
61
72
8ReferenceError: indiceNovo is not definedUma variável vazada pode parecer inofensiva, mas quer dizer que um contador de laço ou um valor temporário pode se chocar com outra variável de mesmo nome em outro ponto da função. O escopo de bloco deixa cada variável no seu lugar.
O erro clássico do laço
Este é o erro que fez muita gente trocar de vez. Cada volta do laço agenda uma função para rodar um instante depois:
1for (var indice = 0; indice < 3; indice++) {
2 setTimeout(() => console.log("var:", indice), 0);
3}
4
5for (let indice = 0; indice < 3; indice++) {
6 setTimeout(() => console.log("let:", indice), 0);
7}1var: 3
2var: 3
3var: 3
4let: 0
5let: 1
6let: 2
As funções rodam depois que o laço terminou. Com var existe um único indice para o laço inteiro e, nessa hora, ele vale 3. Com let, o JavaScript cria um indice novo para cada volta, então cada função lembra o seu próprio valor. O mesmo erro aparece com tratadores de clique, temporizadores e promises criados num laço, e o let conserta todos sem nenhum código a mais.
Hoisting e a zona morta temporal
O JavaScript prepara as variáveis de uma função antes de rodar a primeira linha dela. Isso se chama hoisting ("içamento"). O que uma variável guarda antes da linha da sua declaração é onde as palavras-chave diferem:
1function antesDeDeclarar() {
2 console.log("var antes da linha:", cedo);
3 var cedo = "pronto";
4 console.log("var depois da linha:", cedo);
5
6 try {
7 console.log(tarde);
8 } catch (erro) {
9 console.log(`${erro.name}: ${erro.message}`);
10 }
11 let tarde = "pronto";
12}
13
14antesDeDeclarar();var antes da linha: undefined
var depois da linha: pronto
ReferenceError: Cannot access 'tarde' before initialization
Um var existe desde o topo da função e guarda undefined em silêncio até a sua linha rodar, então um engano dá um valor errado em vez de um erro. Um let ou const também existe desde o topo do seu bloco, mas mexer nele antes da sua linha gera um erro ("não é possível acessar 'tarde' antes da inicialização"). Esse trecho se chama zona morta temporal (temporal dead zone). Erros barulhentos são bons: apontam para o engano em vez de deixar o undefined viajar pelo programa.
Declarando o mesmo nome duas vezes
O var deixa você declarar uma variável de novo sem um pio, o que pode sobrescrever em silêncio uma que já está em uso:
var pontos = 10;
var pontos = 0;
console.log(pontos);0Com let ou const, o programa se recusa a rodar:
let pontos = 10;
let pontos = 0;SyntaxError: Identifier 'pontos' has already been declared("O identificador 'pontos' já foi declarado.") E mais uma esquisitice do var: num script comum de navegador, um var declarado no nível de cima vira uma propriedade do objeto global window, onde qualquer outro script da página pode lê-lo ou sobrescrevê-lo. Um let ou const no nível de cima não vira.
Algum deles é mais rápido?
Não. Medimos o mesmo laço de soma escrito dos três jeitos, com 50 milhões de voltas:

Os três deram cerca de 1,3 nanossegundo por volta, e as pequenas diferenças mudavam de uma execução para outra. Os motores de JavaScript compilam os três para o mesmo código rápido, então escolha só pela clareza e pela correção.
Lado a lado
var | let | const | |
|---|---|---|---|
| Pode ser reatribuída | Sim | Sim | Não |
| Precisa de um valor na hora | Não | Não | Sim |
| Escopo | A função inteira | O bloco { } mais próximo | O bloco { } mais próximo |
| Cópia nova a cada volta do laço | Não (uma variável compartilhada) | Sim | Sim (no for…of / for…in) |
| Usada antes da sua linha | undefined | ReferenceError | ReferenceError |
| Declarada duas vezes | Permitido | SyntaxError | SyntaxError |
No nível de cima vira propriedade do window | Sim | Não | Não |
| Usar em código novo | Não | Para reatribuir | Por padrão |

Modernizando código antigo
Converter código com var é quase mecânico. Pegue esta função:
1function resumir(precos) {
2 var total = 0;
3 var quantidade = precos.length;
4 for (var indice = 0; indice < quantidade; indice++) {
5 total += precos[indice];
6 }
7 var media = total / quantidade;
8 return "Total " + total + ", média " + media.toFixed(2);
9}
10
11console.log(resumir([3.5, 4.25, 2]));Total 9.75, média 3.25Passe por cada var: ela é reatribuída em algum momento depois da declaração? total é (+=) e indice também (++): viram let. quantidade e media nunca são: viram const.
1function resumir(precos) {
2 let total = 0;
3 const quantidade = precos.length;
4 for (let indice = 0; indice < quantidade; indice++) {
5 total += precos[indice];
6 }
7 const media = total / quantidade;
8 return `Total ${total}, média ${media.toFixed(2)}`;
9}
10
11console.log(resumir([3.5, 4.25, 2]));Total 9.75, média 3.25A mesma saída, e agora cada declaração diz a quem lê se o valor muda ou não. (A template string com crases é outra melhoria do ES6.)
Sua vez: pratique
Tente fazer estes antes de espiar as respostas.
Exercício 1: Adivinhe a saída (aquecimento). O que as três linhas com console.log imprimem?
1const cores = ["vermelho"];
2cores.push("verde");
3console.log(cores.length);
4
5let contagem = 1;
6contagem = contagem + 1;
7console.log(contagem);
8
9var tom = "escuro";
10if (true) {
11 var tom = "claro";
12}
13console.log(tom);Exercício 2: const ou let? Escolha uma palavra-chave para cada variável: o ano de nascimento de um usuário, uma pontuação que sobe durante um jogo, uma lista de tarefas em que você vai acrescentando itens e o resultado de Math.max(...numeros).
Exercício 3: Conserte os botões. Este código deveria informar "Clicou no botão 0", "Clicou no botão 1" e "Clicou no botão 2", mas todo tratador informa o botão 3. Conserte.
1const tratadores = [];
2for (var numeroDoBotao = 0; numeroDoBotao < 3; numeroDoBotao++) {
3 tratadores.push(() => console.log("Clicou no botão " + numeroDoBotao));
4}
5tratadores.forEach((tratador) => tratador());Exercício 4: Ache o erro. O que acontece quando isto roda, e por quê?
1function cumprimentar() {
2 console.log(mensagem);
3 const mensagem = "Oi!";
4}
5cumprimentar();Exercício 5: Modernize (desafio). Troque cada var por const ou let, escolhendo const sempre que der, e garanta que a saída não mude.
1var palavras = ["árvore", "céu", "abacaxi", "sol"];
2var maior = "";
3for (var indice = 0; indice < palavras.length; indice++) {
4 var palavra = palavras[indice];
5 if (palavra.length > maior.length) {
6 maior = palavra;
7 }
8}
9console.log("Maior palavra: " + maior);Quiz rápido
- Dá para fazer
pushnum array declarado comconst? - Onde existe uma variável declarada com
letdentro de um blocoif? - Por que um laço com
varesetTimeoutimprime o mesmo número três vezes? - O que você ganha ao usar um
varantes da linha da sua declaração? E umlet? - Com qual palavra-chave você deve começar?
Respostas
Exercício 1: O push muda o conteúdo do array, o que o const permite; contagem é reatribuída, o que o let permite; e o segundo var tom nem é uma variável nova, porque o var ignora o bloco do if e só reatribui o mesmo tom:
1const cores = ["vermelho"];
2cores.push("verde");
3console.log(cores.length);
4
5let contagem = 1;
6contagem = contagem + 1;
7console.log(contagem);
8
9var tom = "escuro";
10if (true) {
11 var tom = "claro";
12}
13console.log(tom);2
2
claroCom let tom nas duas vezes, a de dentro seria outra variável, vivendo só no bloco, e a última linha imprimiria escuro.
Exercício 2: Ano de nascimento: const (nunca muda). Pontuação do jogo: let (pontos += 10 a reatribui). Lista de tarefas: const (o push muda o conteúdo, não a variável). Math.max(...numeros): const (é calculado uma vez).
Exercício 3: Troque var por let, para cada volta ganhar o seu próprio numeroDoBotao:
1const tratadores = [];
2for (let numeroDoBotao = 0; numeroDoBotao < 3; numeroDoBotao++) {
3 tratadores.push(() => console.log("Clicou no botão " + numeroDoBotao));
4}
5tratadores.forEach((tratador) => tratador());Clicou no botão 0
Clicou no botão 1
Clicou no botão 2Exercício 4: Dá erro, porque mensagem é usada dentro da sua zona morta temporal, antes da linha do const:
1function cumprimentar() {
2 try {
3 console.log(mensagem);
4 } catch (erro) {
5 console.log(`${erro.name}: ${erro.message}`);
6 }
7 const mensagem = "Oi!";
8}
9cumprimentar();ReferenceError: Cannot access 'mensagem' before initializationCom var mensagem, imprimiria undefined e esconderia o engano. O conserto é declarar a variável antes de usá-la.
Exercício 5: palavras nunca é reatribuída, então é const (só a lemos). maior muda dentro do laço, então é let. indice é reatribuído pelo indice++: let. palavra é uma variável nova a cada volta e nunca é reatribuída dentro de uma: const.
1const palavras = ["árvore", "céu", "abacaxi", "sol"];
2let maior = "";
3for (let indice = 0; indice < palavras.length; indice++) {
4 const palavra = palavras[indice];
5 if (palavra.length > maior.length) {
6 maior = palavra;
7 }
8}
9console.log("Maior palavra: " + maior);Maior palavra: abacaxiMais caprichado ainda: for (const palavra of palavras) elimina o índice de vez.
Quiz: (1) Sim: o const impede reatribuir a variável, não mudar o conteúdo do array. (2) Só dentro daquele bloco, entre as suas chaves, da linha da declaração em diante. (3) Existe uma única variável var para o laço inteiro, e os callbacks rodam depois que o laço termina, quando ela já vale 3. (4) Um var dá undefined; um let gera ReferenceError: Cannot access … before initialization. (5) const.
Cola rápida
| Situação | Use |
|---|---|
| Um valor que você define uma vez | const |
| Um array ou objeto em que você acrescenta ou muda coisas | const |
| Um contador, total ou indicador que você reatribui | let |
O índice de um laço clássico for (… ; … ; indice++) | let |
A variável de um laço for…of | const |
Um valor escolhido por um if / else | let (ou const com o operador ? :) |
| Um conteúdo que nunca pode mudar | const mais Object.freeze |
| Código novo | Nunca var |
Resumindo
const e let não são só jeitos novos de escrever var; eles consertam as maiores armadilhas dele:
- Comece com
const, e troque paraletsó quando reatribuir com=. - O
consttranca a variável, não o valor: arrays e objetos ainda podem mudar por dentro. leteconsttêm escopo de bloco; ovarvaza dos blocos para a função inteira.- Laços com
letdão a cada volta a sua própria variável, consertando o erro de "todos os meus callbacks veem 3". - Usar
letouconstcedo demais gera um erro (a zona morta temporal) em vez de entregarundefined. - Redeclarar é um erro com
leteconst. - A velocidade é a mesma: cerca de 1,3 nanossegundo por volta de laço para os três no nosso teste.
Para os detalhes, veja as páginas do MDN sobre const, let e var.
Leia a seguir

O Que É a Linguagem de Programação Rust? Um Guia Simples da Linguagem Rápida Que Não Deixa Você Travar
Um guia simples e sem jargões sobre o Rust, a linguagem tão rápida quanto o C que impede famílias inteiras de erros de memória antes de o programa rodar. Entenda a ideia de posse, por que gigantes da tecnologia o adotam e onde ele é usado.

O Que É a Linguagem de Programação Go? Um Guia Simples da Linguagem da Nuvem
Um guia simples e sem jargões sobre o Go, a linguagem simples e rápida que o Google criou para a nuvem. Entenda o que são goroutines, por que um programa Go é um arquivo só e como o Go passou a mover o Docker, o Kubernetes e boa parte da internet.

O Que É a Linguagem de Programação Ruby? Um Guia Simples da Linguagem Feita para a Felicidade
Um guia simples e sem jargões sobre o Ruby, a linguagem de programação feita para deixar os programadores felizes. Entenda por que o código Ruby parece inglês, como o Ruby on Rails move sites como GitHub e Shopify, e onde o Ruby brilha.
Comentários
Seja gentil e específico. Comentários com links de contas novas aparecem depois da análise de um moderador.