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

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.

Rodolfo Mori8 min de leitura

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:

js
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');
não deu para ler a resposta: Expected ',' or '}' after property value in JSON at position 31 (line 1 column 32) 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.

js
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 : TypeError message : Cannot read properties of null (reading 'itens') é Error : true stack[0]: TypeError: Cannot read properties of null (reading 'itens')

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:

js
function totalDoCarrinho(json) {
  try {
    return JSON.parse(json).total;
  } catch {
    return 0;
  }
}

console.log(totalDoCarrinho('{"total": 289.9}'));
console.log(totalDoCarrinho('resposta truncada'));
289.9 0

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:

js
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());
}
Error | CEP inválido: 0131 at validarCep (file:///private/tmp/loja/t04.mjs:3:11)

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:

js
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);
}
typeof : string name : undefined stack : undefined

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:

js
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;
    }
  }
}
BLACKFRIDAY -> 140 NATAL2026 -> recusado: cupom "NATAL2026" não existe

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.

js
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));
[log] Teclado mecânico — estoque agora: 1 baixa aplicada [log] Teclado mecânico — estoque agora: 1 falhou: estoque insuficiente: pedidas 9, restam 1

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.

js
function calcularFrete(peso) {
  try {
    return peso * 1.5 + 12.5;
  } finally {
    return 0;
  }
}

console.log(calcularFrete(10));
0

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:

js
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);
}
topo : Não foi possível carregar o pedido 4821 causa : SyntaxError — Unexpected end of JSON input

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:

js
try {
  setTimeout(() => {
    const pedido = undefined;
    console.log(pedido.total);
  }, 0);
} catch (erro) {
  console.log('peguei:', erro.message);
}
file:///private/tmp/loja/t09.mjs:4 console.log(pedido.total); ^ TypeError: Cannot read properties of undefined (reading 'total') at Timeout._onTimeout (file:///private/tmp/loja/t09.mjs:4:24) at listOnTimeout (node:internal/timers:605:17) at process.processTimers (node:internal/timers:541:7) Node.js v24.16.0

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. setTimeoutagenda 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:

js
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);
}
achei que tinha dado certo file:///private/tmp/loja/t11.mjs:3 setTimeout(() => reject(new Error(`pagamento de R$ ${valor} recusado`)), 10); ^ Error: pagamento de R$ 289.9 recusado at Timeout._onTimeout (file:///private/tmp/loja/t11.mjs:3:29) at listOnTimeout (node:internal/timers:605:17) at process.processTimers (node:internal/timers:541:7) Node.js v24.16.0

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:

js
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);
}
peguei: pagamento de R$ 289.9 recusado

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.parse sabe que o JSON quebrou; ela não sabe se isso deve virar uma tela de erro ou um valor padrão. Use Error.cause para levar o diagnóstico até quem decide.
  • Não use try/catch para fluxo normal. Se o cupom pode não existir, um if é mais barato e mais legível que uma exceção. Erro é para o que não deveria acontecer.
  • Nunca deixe um catch vazio. Um catch {} 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.

  • try catch
  • erros
  • throw
  • finally
  • assincrono

Perguntas frequentes

Posso envolver o programa inteiro num try/catch?
Pode, e não resolve nada. Um try gigante captura tudo no mesmo lugar, sem saber qual das trinta linhas falhou, e o catch acaba virando um log genérico. Envolva a operação que você sabe que pode falhar — a leitura do JSON, a chamada de rede, a conversão de data — e nada mais.
Por que meu catch não pegou o erro do setTimeout?
Porque quando o callback do setTimeout roda, o bloco try já terminou há muito tempo. O try protege a pilha de chamadas do momento, não o futuro. Para código assíncrono, o try só funciona em volta de um await, ou dentro do próprio callback.
throw de string funciona?
Funciona no sentido de interromper a execução, e é péssimo. Uma string não tem name, não tem stack e não passa em nenhum instanceof. Quem receber esse valor no catch não consegue nem descobrir de que linha ele veio. Lance sempre um Error ou uma subclasse dele.
Preciso de catch se já tenho finally?
Não precisa. try/finally sem catch é válido e é o padrão quando você quer garantir a limpeza (fechar conexão, liberar o botão da tela) mas quer que o erro continue subindo para quem chamou. O finally roda e o erro segue viagem.

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 — try...catch — developer.mozilla.org
  2. MDN — Error: cause — developer.mozilla.org
  3. ECMAScript 2026 Language Specification — The try Statement — tc39.es

Continue por aqui