try catch em JavaScript: tratamento de erros
Como o try/catch funciona, por que ele não pega erro dentro de setTimeout, o que finally faz com o return e como usar Error.cause — tudo rodado no Node.
try/catch é o jeito de dizer ao JavaScript: “tente rodar isto; se explodir,
não derrube o programa — me entregue o erro e siga daqui”. O bloco try guarda
o código arriscado, o catch recebe o objeto de erro, e a execução continua na
linha seguinte em vez de morrer.
A resposta curta, para você já sair usando: envolva só a operação que pode
falhar, sempre lance new Error(...) e nunca uma string, e lembre que
try/catch não enxerga nada que aconteça depois — erro dentro de setTimeout
escapa. O resto deste texto é o porquê, com cada caso rodando no terminal.
Os exemplos são de uma loja virtual: resposta de API, cupom, baixa de estoque, confirmação de pagamento.
try/catch é a bandeja sob o trabalho arriscado
Quando você abre um vidro de tinta sobre uma bandeja, o objetivo não é impedir
que ele caia. A bandeja limita o estrago e entrega a sujeira num lugar onde você
consegue tratá-la. O bloco try delimita a operação arriscada; quando ela lança
um erro, o catch recebe esse objeto e decide o que fazer.
A analogia termina no tempo: uma bandeja física continua ali, mas o
try/catch só captura erros lançados durante a execução síncrona daquele bloco.
Uma função agendada para depois já está fora dele. Antes de ver a solução,
marque no primeiro exemplo a menor linha que pode falhar e envolva apenas essa
linha. Quanto menor o try, mais claro fica qual operação quebrou e menor o
risco de esconder um erro de programação como se fosse uma falha esperada.
A operação que pode falhar
O caso clássico é ler um JSON que veio truncado da rede. Sem try, o programa
para na hora. Com try, ele sabe o que fazer:
const respostaDaApi = '{"pedido": 4821, "total": 289.9';
try {
const pedido = JSON.parse(respostaDaApi);
console.log('total do pedido:', pedido.total);
} catch (erro) {
console.log('não deu para ler a resposta:', erro.message);
}
console.log('o programa continua');Repare em três coisas. A const pedido só existe dentro do try — é um bloco
como qualquer outro. O console.log do try nunca rodou, porque a linha acima
dele interrompeu. E a última linha rodou normalmente: o catch absorveu o
estrago.
O que chega no catch
O parâmetro do catch recebe o valor que foi lançado. Quando o erro vem do
próprio motor, esse valor é sempre um objeto Error com três propriedades que
interessam: name, message e stack.
try {
const carrinho = null;
carrinho.itens.push('Teclado mecânico');
} catch (erro) {
console.log('name :', erro.name);
console.log('message :', erro.message);
console.log('é Error :', erro instanceof Error);
console.log('stack[0]:', erro.stack.split('\n')[0]);
}name é a família do erro (aqui, TypeError), message é a descrição e
stack é o caminho de chamadas até o ponto da falha. A primeira linha do
stack repete name: message; as seguintes são o rastro. Cada família tem um
significado próprio, e vale conhecer
os tipos de erro em JavaScript
antes de sair capturando tudo.
Se você não vai usar o objeto, pode omitir o parâmetro por completo. O catch
sem binding existe desde 2019 e evita a variável erro acusada de não usada
pelo linter:
function totalDoCarrinho(json) {
try {
return JSON.parse(json).total;
} catch {
return 0;
}
}
console.log(totalDoCarrinho('{"total": 289.9}'));
console.log(totalDoCarrinho('resposta truncada'));Lançar o seu próprio erro
throw interrompe a função na hora e procura o catch mais próximo subindo a
pilha. É assim que uma função de validação avisa que recebeu lixo:
function validarCep(cep) {
if (!/^\d{8}$/.test(cep)) {
throw new Error(`CEP inválido: ${cep}`);
}
return cep;
}
try {
validarCep('0131');
} catch (erro) {
console.log(erro.name, '|', erro.message);
console.log(erro.stack.split('\n')[1].trim());
}A segunda linha é o que torna new Error insubstituível: ela aponta o arquivo,
a função e a coluna exata onde o throw aconteceu. Isso é o stack, e ele é
montado no momento em que o objeto Error é construído.
Nunca lance uma string
Tecnicamente você pode lançar qualquer valor: número, objeto solto, string. Tecnicamente também dá para dirigir com o vidro embaçado. Veja o que sobra:
function validarCep(cep) {
if (!/^\d{8}$/.test(cep)) throw `CEP inválido: ${cep}`;
return cep;
}
try {
validarCep('0131');
} catch (erro) {
console.log('typeof :', typeof erro);
console.log('name :', erro.name);
console.log('stack :', erro.stack);
}Sem name, sem stack, e erro instanceof Error daria false. Quem receber
esse valor três funções acima não tem como descobrir de onde ele veio, e
qualquer catch que faça erro.message.includes(...) quebra com um
TypeError novo em folha por cima do erro original.
A regra é seca: o que vai depois de throw é sempre um Error. Quando você
precisa carregar informação extra, estenda a classe em vez de trocar o tipo:
const CUPONS = { PRIMEIRACOMPRA: 10, BLACKFRIDAY: 30 };
class CupomInvalidoError extends Error {
constructor(codigo) {
super(`cupom "${codigo}" não existe`);
this.name = 'CupomInvalidoError';
this.codigo = codigo;
}
}
function aplicarCupom(valor, codigo) {
const percentual = CUPONS[codigo];
if (percentual === undefined) throw new CupomInvalidoError(codigo);
return valor - valor * (percentual / 100);
}
for (const codigo of ['BLACKFRIDAY', 'NATAL2026']) {
try {
console.log(codigo, '->', aplicarCupom(200, codigo));
} catch (erro) {
if (erro instanceof CupomInvalidoError) {
console.log(codigo, '-> recusado:', erro.message);
} else {
throw erro;
}
}
}O else { throw erro; } é a parte que quase todo mundo esquece. Um catch que
engole tudo transforma um bug de digitação num “cupom recusado” mentiroso.
Capture o erro que você sabe tratar e relance o resto.
finally roda mesmo com return
O finally é executado sempre: quando o try termina bem, quando o catch
trata o erro e — a parte que surpreende — mesmo quando um return já decidiu o
valor da função.
function baixarEstoque(produto, quantidade) {
try {
if (quantidade > produto.estoque) {
throw new RangeError(`estoque insuficiente: pedidas ${quantidade}, restam ${produto.estoque}`);
}
produto.estoque -= quantidade;
return 'baixa aplicada';
} catch (erro) {
return `falhou: ${erro.message}`;
} finally {
console.log(`[log] ${produto.nome} — estoque agora: ${produto.estoque}`);
}
}
const teclado = { nome: 'Teclado mecânico', estoque: 3 };
console.log(baixarEstoque(teclado, 2));
console.log(baixarEstoque(teclado, 9));Leia a ordem da saída com atenção: o [log] do finally aparece antes do
valor devolvido, nas duas chamadas. O return já calculou o que ia entregar,
mas o finally roda antes de a função de fato sair. É por isso que ele é o
lugar certo para fechar conexão, liberar o botão de “Finalizar compra” ou parar
um spinner: não existe caminho de saída que escape dele.
Existe uma armadilha, e ela é grave: se o próprio finally tiver um return,
esse valor substitui o que o try ou o catch iam devolver.
function calcularFrete(peso) {
try {
return peso * 1.5 + 12.5;
} finally {
return 0;
}
}
console.log(calcularFrete(10));O frete de R$ 27,50 virou zero, sem erro nenhum, sem aviso do motor. Pior: se o
try tivesse lançado, o return do finally engoliria o erro também. Regra:
finally limpa, finally não devolve.
Error.cause: guardar o erro de baixo
Quando você captura um erro técnico e relança um erro com linguagem de negócio,
a causa original costuma se perder. O segundo argumento do construtor de Error
resolve isso:
function lerPedido(bruto) {
try {
return JSON.parse(bruto);
} catch (erro) {
throw new Error('Não foi possível carregar o pedido 4821', { cause: erro });
}
}
try {
lerPedido('{"pedido": 4821, "total":');
} catch (erro) {
console.log('topo :', erro.message);
console.log('causa :', erro.cause.name, '—', erro.cause.message);
}A mensagem de cima é a que vai para a tela do cliente. A de baixo é a que vai
para o log e diz ao time o que realmente aconteceu. Sem cause, você escolhe
entre uma das duas; com cause, tem as duas na mesma exceção.
O erro mais comum: setTimeout escapando do try
Este é o caso que faz gente jurar que try/catch está quebrado. O código parece
protegido:
try {
setTimeout(() => {
const pedido = undefined;
console.log(pedido.total);
}, 0);
} catch (erro) {
console.log('peguei:', erro.message);
}O catch nunca imprimiu nada e o processo caiu. O motivo está na própria stack:
o erro aconteceu em Timeout._onTimeout, chamado por listOnTimeout. Não há
nenhuma menção ao try — porque ele já tinha terminado. setTimeout só
agenda a função; a chamada em si acontece depois, a partir do laço de
eventos, numa pilha completamente nova. O try protege a pilha do momento, não
o futuro.
O mesmo vale para uma promessa que você não esperou:
function confirmarPagamento(valor) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error(`pagamento de R$ ${valor} recusado`)), 10);
});
}
try {
confirmarPagamento(289.9);
console.log('achei que tinha dado certo');
} catch (erro) {
console.log('peguei:', erro.message);
}A linha “achei que tinha dado certo” é a prova do crime: para o try, tudo
correu bem. confirmarPagamento devolveu uma promessa pendente e a função
seguiu adiante. A rejeição chegou dez milissegundos depois, sem ninguém
esperando.
Com um await no meio, o mesmo código volta a funcionar:
function confirmarPagamento(valor) {
return new Promise((_, reject) => {
setTimeout(() => reject(new Error(`pagamento de R$ ${valor} recusado`)), 10);
});
}
try {
await confirmarPagamento(289.9);
} catch (erro) {
console.log('peguei:', erro.message);
}await religa a promessa à pilha em que o try está: a rejeição vira uma
exceção lançada exatamente naquela linha. Por isso a regra prática é
try/catch só cobre assíncrono quando existe um await dentro dele. Para
callbacks que você não controla — setTimeout, um listener de evento, um
addEventListener — o try tem que estar dentro do callback, não em volta
dele.
Onde colocar o try/catch na prática
Depois de anos corrigindo código de aluno, três hábitos resolvem quase tudo:
- Capture perto da falha, decida longe dela. A função que faz
JSON.parsesabe que o JSON quebrou; ela não sabe se isso deve virar uma tela de erro ou um valor padrão. UseError.causepara levar o diagnóstico até quem decide. - Não use
try/catchpara fluxo normal. Se o cupom pode não existir, umifé mais barato e mais legível que uma exceção. Erro é para o que não deveria acontecer. - Nunca deixe um
catchvazio. Umcatch {}sem nada dentro é um bug invisível guardado para daqui a seis meses. No mínimo, registre — e se você registrou o erro no console mas não consegue reconstruir o estado que levou até ele, é hora de abrir o depurador do DevTools em vez de espalhar mais logs.
O erro que mais aparece dentro de catch no mundo real continua sendo o
TypeError: Cannot read properties of undefined,
e o motivo quase sempre é o mesmo: alguém confiou numa resposta de API que
chegou incompleta.
A próxima parada da trilha de JavaScript é o mapa completo dos tipos de erro e da stack trace; se quiser rever a base de como uma função devolve e propaga valores antes disso, volte em funções em JavaScript. A ordem de estudo inteira está no guia completo de JavaScript.
Perguntas frequentes
Posso envolver o programa inteiro num try/catch?
Por que meu catch não pegou o erro do setTimeout?
throw de string funciona?
Preciso de catch se já tenho finally?
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 — try...catch — developer.mozilla.org
- MDN — Error: cause — developer.mozilla.org
- ECMAScript 2026 Language Specification — The try Statement — tc39.es


