Todo programa que vai para produção vai encontrar situações inesperadas. O usuário digita um valor inválido. A API não responde. O arquivo não existe. O banco de dados cai. Nenhum desses cenários é um bug — são situações previsíveis que precisam ser tratadas com elegância.
Um código que não trata erros quebra silenciosamente, exibe mensagens técnicas para o usuário ou simplesmente para de funcionar. Um código bem escrito antecipa o que pode dar errado e age de forma controlada.
É isso que vamos aprender neste artigo.
O que é um erro em JavaScript?
Quando o JavaScript encontra um problema que não consegue resolver, ele lança um objeto de erro e interrompe a execução do código — a menos que você capture esse erro.
// Sem tratamento — o programa quebra aqui
const resultado = JSON.parse("isso não é um JSON válido");
console.log("Esta linha nunca executa.");
// SyntaxError: Unexpected token i in JSON at position 0
O erro se propaga para cima na pilha de chamadas até encontrar alguém que o trate — ou até travar o programa.
try e catch — capturando erros
A estrutura básica do tratamento de erros:
try {
// código que pode lançar um erro
const dados = JSON.parse("json inválido");
} catch (erro) {
// código executado SE um erro ocorrer
console.log("Algo deu errado:", erro.message);
}
console.log("O programa continua normalmente.");
// Algo deu errado: Unexpected token j in JSON at position 0
// O programa continua normalmente.
O bloco try envolve o código arriscado. Se qualquer linha dentro dele lançar um erro, a execução pula imediatamente para o catch — que recebe o objeto de erro como parâmetro.
O objeto de erro
O erro capturado é um objeto com propriedades úteis:
try {
null.propriedade; // TypeError
} catch (erro) {
console.log(erro.name); // TypeError
console.log(erro.message); // Cannot read properties of null
console.log(erro.stack); // Stack trace completo (onde o erro ocorreu)
}
As propriedades mais usadas são name (tipo do erro) e message (descrição legível).
Tipos de erros nativos
O JavaScript possui vários tipos de erros embutidos, cada um para uma situação diferente:
// ReferenceError — variável não existe
try {
console.log(variavelInexistente);
} catch (e) {
console.log(e.name); // ReferenceError
}
// TypeError — tipo errado para a operação
try {
null.metodo();
} catch (e) {
console.log(e.name); // TypeError
}
// SyntaxError — código ou dado mal formado
try {
JSON.parse("{chave sem aspas: valor}");
} catch (e) {
console.log(e.name); // SyntaxError
}
// RangeError — valor fora do intervalo permitido
try {
new Array(-1);
} catch (e) {
console.log(e.name); // RangeError
}
finally — executar sempre
O bloco finally executa sempre, independente de ter ocorrido um erro ou não. É ideal para código de limpeza que deve rodar de qualquer jeito:
function lerArquivo(nome) {
console.log(`Abrindo arquivo: ${nome}`);
try {
if (nome !== "dados.json") {
throw new Error("Arquivo não encontrado.");
}
console.log("Arquivo lido com sucesso!");
return "conteúdo do arquivo";
} catch (erro) {
console.log(`Erro: ${erro.message}`);
return null;
} finally {
// executa sempre — com ou sem erro
console.log("Fechando conexão com o arquivo.");
}
}
lerArquivo("dados.json");
// Abrindo arquivo: dados.json
// Arquivo lido com sucesso!
// Fechando conexão com o arquivo.
lerArquivo("outro.txt");
// Abrindo arquivo: outro.txt
// Erro: Arquivo não encontrado.
// Fechando conexão com o arquivo.
Na prática, finally é muito usado para fechar conexões com banco de dados, esconder indicadores de carregamento ou liberar recursos.
throw — lançando seus próprios erros
Você não precisa esperar o JavaScript lançar um erro — pode lançar os seus próprios com throw:
function dividir(a, b) {
if (b === 0) {
throw new Error("Divisão por zero não é permitida.");
}
return a / b;
}
try {
console.log(dividir(10, 2)); // 5
console.log(dividir(10, 0)); // lança erro
} catch (erro) {
console.log(`Erro capturado: ${erro.message}`);
}
// 5
// Erro capturado: Divisão por zero não é permitida.
Você pode lançar qualquer valor com throw — mas a convenção é sempre lançar um objeto Error para manter consistência.
Criando erros personalizados
Para sistemas maiores, é útil criar tipos de erro específicos estendendo a classe Error:
class ErroValidacao extends Error {
constructor(campo, mensagem) {
super(mensagem);
this.name = "ErroValidacao";
this.campo = campo;
}
}
class ErroAutenticacao extends Error {
constructor(mensagem) {
super(mensagem);
this.name = "ErroAutenticacao";
}
}
function validarEmail(email) {
if (!email.includes("@")) {
throw new ErroValidacao("email", `"${email}" não é um e-mail válido.`);
}
return true;
}
function autenticar(usuario, senha) {
if (senha.length < 6) {
throw new ErroAutenticacao("Senha deve ter pelo menos 6 caracteres.");
}
return true;
}
// Tratando erros de tipos diferentes
function processarLogin(email, senha) {
try {
validarEmail(email);
autenticar(email, senha);
console.log("Login realizado com sucesso!");
} catch (erro) {
if (erro instanceof ErroValidacao) {
console.log(`Campo inválido (${erro.campo}): ${erro.message}`);
} else if (erro instanceof ErroAutenticacao) {
console.log(`Falha de autenticação: ${erro.message}`);
} else {
console.log(`Erro inesperado: ${erro.message}`);
}
}
}
processarLogin("emailsemarroba.com", "123456");
// Campo inválido (email): "emailsemarroba.com" não é um e-mail válido.
processarLogin("user@email.com", "123");
// Falha de autenticação: Senha deve ter pelo menos 6 caracteres.
processarLogin("user@email.com", "senha123");
// Login realizado com sucesso!
Validação defensiva — errar cedo e com clareza
Um princípio de código limpo: valide as entradas no início da função e lance erros claros antes de continuar:
function criarUsuario({ nome, email, idade }) {
// Valide tudo antes de qualquer processamento
if (!nome || typeof nome !== "string") {
throw new Error("Nome é obrigatório e deve ser texto.");
}
if (!email || !email.includes("@")) {
throw new Error("E-mail inválido.");
}
if (!Number.isInteger(idade) || idade < 0 || idade > 130) {
throw new Error("Idade deve ser um número inteiro entre 0 e 130.");
}
// Só chegamos aqui se tudo estiver válido
return {
id: Math.random().toString(36).slice(2),
nome: nome.trim(),
email: email.toLowerCase(),
idade,
criadoEm: new Date().toISOString(),
};
}
try {
const usuario = criarUsuario({ nome: " Ana ", email: "ana@email.com", idade: 25 });
console.log(usuario);
} catch (erro) {
console.log(`Erro ao criar usuário: ${erro.message}`);
}
Erros silenciosos — o que evitar
Um anti-padrão perigoso é capturar erros e não fazer nada com eles:
// ❌ Nunca faça isso — engole o erro silenciosamente
try {
operacaoArriscada();
} catch (e) {
// silêncio total
}
// ❌ Também ruim — captura mas não trata corretamente
try {
operacaoArriscada();
} catch (e) {
console.log("deu erro"); // sem detalhes, sem ação
}
// ✅ Trate com intenção
try {
operacaoArriscada();
} catch (e) {
console.error(`[ERRO] ${e.name}: ${e.message}`);
// notificar sistema de monitoramento
// exibir mensagem útil ao usuário
// tentar uma alternativa (fallback)
}
Boas práticas de tratamento de erros
// ✅ 1. Seja específico — não trate tudo como erro genérico
catch (erro) {
if (erro instanceof TypeError) { /* ... */ }
if (erro instanceof RangeError) { /* ... */ }
}
// ✅ 2. Mensagens de erro úteis para o desenvolvedor
throw new Error(`Usuário com id ${id} não encontrado na base de dados.`);
// ✅ 3. Não lance erros para fluxo de controle normal
// Se "não encontrar" é esperado, retorne null — não lance erro
function buscarUsuario(id) {
const usuario = banco.find(u => u.id === id);
return usuario || null; // não lance erro para ausência esperada
}
// ✅ 4. Sempre use finally para limpeza de recursos
try {
conexao.abrir();
conexao.executar(query);
} catch (erro) {
console.error(erro);
} finally {
conexao.fechar(); // fecha sempre, com ou sem erro
}
Exemplo completo — sistema de cadastro
class ErroCadastro extends Error {
constructor(mensagem, campo = null) {
super(mensagem);
this.name = "ErroCadastro";
this.campo = campo;
}
}
function validarCadastro(dados) {
const { nome, email, senha, confirmacaoSenha } = dados;
if (!nome || nome.trim().length < 2) {
throw new ErroCadastro("Nome deve ter pelo menos 2 caracteres.", "nome");
}
if (!email || !email.includes("@")) {
throw new ErroCadastro("E-mail inválido.", "email");
}
if (!senha || senha.length < 8) {
throw new ErroCadastro("Senha deve ter pelo menos 8 caracteres.", "senha");
}
if (senha !== confirmacaoSenha) {
throw new ErroCadastro("As senhas não coincidem.", "confirmacaoSenha");
}
return true;
}
function cadastrarUsuario(dados) {
console.log("Iniciando cadastro...");
try {
validarCadastro(dados);
console.log(`Usuário "${dados.nome}" cadastrado com sucesso!`);
return { sucesso: true };
} catch (erro) {
if (erro instanceof ErroCadastro) {
console.log(`Erro no campo "${erro.campo}": ${erro.message}`);
} else {
console.log(`Erro inesperado: ${erro.message}`);
}
return { sucesso: false, erro: erro.message };
} finally {
console.log("Processo de cadastro finalizado.\n");
}
}
cadastrarUsuario({ nome: "A", email: "email@ok.com", senha: "12345678", confirmacaoSenha: "12345678" });
// Iniciando cadastro...
// Erro no campo "nome": Nome deve ter pelo menos 2 caracteres.
// Processo de cadastro finalizado.
cadastrarUsuario({ nome: "João", email: "emailinvalido", senha: "12345678", confirmacaoSenha: "12345678" });
// Iniciando cadastro...
// Erro no campo "email": E-mail inválido.
// Processo de cadastro finalizado.
cadastrarUsuario({ nome: "João", email: "joao@email.com", senha: "minhasenha", confirmacaoSenha: "minhasenha" });
// Iniciando cadastro...
// Usuário "João" cadastrado com sucesso!
// Processo de cadastro finalizado.
Tarefa para você
Construa uma função calcularMedia(notas) robusta que:
- Lance um erro se
notasnão for um array - Lance um erro se o array estiver vazio
- Lance um erro se alguma nota não for um número entre 0 e 10
- Retorne a média calculada se tudo estiver correto
- Trate todos os erros com mensagens claras e específicas
// Esperado:
calcularMedia([8, 7, 9]); // Média: 8.00
calcularMedia([]); // Erro: array vazio
calcularMedia("não sou um array"); // Erro: notas deve ser um array
calcularMedia([8, 15, 7]); // Erro: nota 15 está fora do intervalo
Ver solução — calcularMedia com erros nomeados e mensagem que aponta o culpado
// ---------------------------------------------------------------
// Erros próprios: quem chama consegue tratar por TIPO, não por texto
// ---------------------------------------------------------------
// Comparar `erro.message` com string quebra assim que alguém corrige
// uma vírgula na mensagem. Comparar o tipo, não.
class ErroDeValidacao extends Error {
constructor(mensagem, campo) {
super(mensagem);
this.name = "ErroDeValidacao";
this.campo = campo;
}
}
// ---------------------------------------------------------------
// 1 a 4 — a função
// ---------------------------------------------------------------
function calcularMedia(notas) {
// 1. tem de ser array. typeof [] é "object", então typeof não serve:
// quem responde isso é Array.isArray.
if (!Array.isArray(notas)) {
throw new ErroDeValidacao(
`notas deve ser um array (recebi ${typeof notas})`,
"notas"
);
}
// 2. array vazio: dividir por zero daria NaN, que se espalha calado
// pelo resto do sistema em vez de estourar aqui.
if (notas.length === 0) {
throw new ErroDeValidacao("o array de notas está vazio", "notas");
}
// 3. cada nota, com o ÍNDICE na mensagem — em array de 40 notas,
// "nota 15 inválida" sem posição não ajuda ninguém a corrigir.
notas.forEach((nota, indice) => {
// Number.isFinite recusa NaN, Infinity e string de uma vez.
// typeof NaN é "number", então testar só o tipo deixaria passar.
if (!Number.isFinite(nota)) {
throw new ErroDeValidacao(
`a nota da posição ${indice} não é um número: ${JSON.stringify(nota)}`,
`notas[${indice}]`
);
}
if (nota < 0 || nota > 10) {
throw new ErroDeValidacao(
`a nota ${nota} (posição ${indice}) está fora do intervalo 0–10`,
`notas[${indice}]`
);
}
});
// 4. tudo certo
return notas.reduce((soma, nota) => soma + nota, 0) / notas.length;
}
// ---------------------------------------------------------------
// 5 — tratando os erros
// ---------------------------------------------------------------
function exibirMedia(notas) {
try {
const media = calcularMedia(notas);
console.log(`Média: ${media.toFixed(2)}`);
return media;
} catch (erro) {
// Tratamento por tipo: o que é falha de dado vira aviso ao usuário;
// o que não é, sobe — engolir erro desconhecido esconde bug.
if (erro instanceof ErroDeValidacao) {
console.error(`Erro em "${erro.campo}": ${erro.message}`);
return null;
}
throw erro;
} finally {
// finally roda dos dois jeitos, inclusive quando há return no try.
// É onde se fecha conexão, arquivo, spinner de carregamento.
console.log("--- fim da validação ---");
}
}
exibirMedia([8, 7, 9]); // Média: 8.00
exibirMedia([]); // Erro em "notas": o array de notas está vazio
exibirMedia("não sou um array"); // Erro em "notas": notas deve ser um array (recebi string)
exibirMedia([8, 15, 7]); // Erro em "notas[1]": a nota 15 (posição 1) está fora do intervalo 0–10
exibirMedia([8, null, 7]); // Erro em "notas[1]": a nota da posição 1 não é um número: null
// ---------------------------------------------------------------
// Coletar TODOS os erros, em vez de parar no primeiro
// ---------------------------------------------------------------
// Formulário que aponta um erro por vez faz o usuário enviar seis
// vezes. Aqui as falhas são acumuladas e entregues juntas.
function validarTudo(notas) {
const problemas = [];
notas.forEach((nota, indice) => {
if (!Number.isFinite(nota)) {
problemas.push(`posição ${indice}: não é número`);
} else if (nota < 0 || nota > 10) {
problemas.push(`posição ${indice}: ${nota} fora de 0–10`);
}
});
if (problemas.length > 0) {
// AggregateError existe justamente para isso desde o ES2021.
throw new AggregateError(
problemas.map((p) => new ErroDeValidacao(p, "notas")),
`${problemas.length} nota(s) inválida(s)`
);
}
return notas.reduce((s, n) => s + n, 0) / notas.length;
}
try {
validarTudo([8, 15, "x", -2]);
} catch (erro) {
console.error(erro.message); // 3 nota(s) inválida(s)
erro.errors.forEach((e) => console.error(` · ${e.message}`));
}
// ---------------------------------------------------------------
// O detalhe que morde: throw dentro de callback assíncrono
// ---------------------------------------------------------------
// Este try NÃO pega nada: quando o setTimeout dispara, o bloco já
// terminou e a pilha é outra. O erro sobe como exceção não capturada.
try {
setTimeout(() => {
throw new Error("ninguém me pega aqui");
}, 100);
} catch (erro) {
console.error("este catch nunca roda");
}
// O try/catch precisa estar DENTRO do callback — ou o código precisa
// ser uma Promise, com try/catch em volta do await.
typeof NaN é "number" e typeof [] é "object" — validar com typeof deixa passar os dois casos que mais quebram média. Use Number.isFinite() e Array.isArray().
Capturar um erro só vale a pena quando há algo a fazer com ele — e é por isso que o catch vazio é pior do que não ter try nenhum: ele troca uma falha ruidosa por um comportamento errado silencioso. Classes de erro próprias servem ao mesmo propósito pelo outro lado, deixando quem chama distinguir o que aconteceu sem precisar interpretar a mensagem. E o finally existe para o que tem de acontecer nos dois caminhos, como fechar o que foi aberto.
Fontes e Referências
- MDN Web Docs — try...catch: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Statements/try...catch
- MDN Web Docs — Error: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Global_Objects/Error
- MDN Web Docs — throw: https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Statements/throw
- JavaScript.info — Error Handling: https://javascript.info/try-catch
- JavaScript.info — Custom Errors: https://javascript.info/custom-errors
- Eloquent JavaScript, Cap. 8 — Bugs and Errors: https://eloquentjavascript.net/08_error.html
- Clean Code — Robert C. Martin, Cap. 7: Error Handling (Alta Books)
Exercícios
Exercício 1
Em que ordem as três linhas aparecem, e o que a função devolve? O finally roda antes ou depois do return?
function lerArquivo(nome) {
console.log(`Abrindo: ${nome}`);
try {
if (nome !== "dados.json") throw new Error("Arquivo não encontrado.");
return "conteúdo";
} catch (erro) {
console.log(`Erro: ${erro.message}`);
return null;
} finally {
console.log("Fechando conexão.");
}
}
console.log(lerArquivo("dados.json"));
Ver resposta
✓ Resposta: Saem Abrindo: dados.json, Fechando conexão. e conteúdo, nessa ordem. O finally roda depois de o valor de retorno já estar calculado, mas antes de ele ser entregue a quem chamou — por isso "Fechando conexão." aparece antes do conteúdo impresso lá fora. É justamente essa garantia que torna o finally o lugar certo para fechar conexão, esconder o indicador de carregamento e liberar recurso: ele acontece nos dois caminhos, com erro ou sem.
Exercício 2
Este catch é considerado um anti-padrão. Descreva o que o usuário e o desenvolvedor veem quando salvarPedido falha — e reescreva o bloco.
try {
salvarPedido(pedido);
mostrarMensagem("Pedido salvo!");
} catch (e) {
}
Ver resposta
✓ Resposta: Ninguém vê nada. O usuário fica sem a confirmação e sem o aviso de falha — a tela simplesmente não reage, e ele tende a tentar de novo, gerando pedidos duplicados. O desenvolvedor não recebe log nem alerta: o erro foi capturado e descartado, e o rastro se perdeu. Uma reescrita mínima: catch (e) { console.error(`[ERRO] ${e.name}: ${e.message}`); mostrarMensagem("Não foi possível salvar. Tente novamente."); } — registrar o detalhe técnico para quem depura e dar ao usuário uma frase acionável.
Exercício 3
Alguém reordenou as verificações do catch. Com um ErroValidacao sendo lançado, qual mensagem aparece — e por quê?
try {
validarEmail("emailsemarroba.com");
} catch (erro) {
if (erro instanceof Error) {
console.log(`Erro inesperado: ${erro.message}`);
} else if (erro instanceof ErroValidacao) {
console.log(`Campo inválido (${erro.campo}): ${erro.message}`);
}
}
Ver resposta
✓ Resposta: Aparece Erro inesperado: "emailsemarroba.com" não é um e-mail válido. — e o segundo ramo nunca será alcançado. Como ErroValidacao extends Error, todo ErroValidacao também é um Error, e o primeiro instanceof já casa. A regra é a mesma da cadeia de else if: vá do mais específico para o mais genérico, deixando Error por último, como rede de segurança. O sintoma aqui é traiçoeiro porque nada quebra — só a mensagem útil, com o nome do campo, se perde.
Exercício 4
O que a linha do catch imprime? O artigo recomenda outra coisa — qual, e por quê?
try {
throw "Divisão por zero não é permitida.";
} catch (erro) {
console.log(erro.message);
}
Ver resposta
✓ Resposta: Imprime undefined. O JavaScript deixa você lançar qualquer valor, inclusive uma string — mas uma string não tem .message, nem .name, nem .stack. Quem captura fica sem a mensagem e sem saber de onde o erro veio. Por isso a convenção: lance sempre new Error("...") (ou uma classe que o estenda). Assim quem trata pode contar com message, com name e com o rastro de pilha, sem precisar adivinhar o formato do que foi lançado.
Exercício 5
Cada trecho lança um tipo diferente de erro nativo. Nomeie os quatro.
console.log(variavelInexistente); // A
null.metodo(); // B
JSON.parse("{chave sem aspas: 1}"); // C
new Array(-1); // D
Ver resposta
✓ Resposta: A é ReferenceError — o nome não existe em nenhum escopo. B é TypeError — o valor existe, mas é de um tipo que não suporta a operação; null não tem métodos. C é SyntaxError — o texto não é um JSON válido, porque as chaves precisam vir entre aspas. D é RangeError — o valor é do tipo certo, mas está fora do intervalo aceito: um array não pode ter comprimento negativo. Reconhecer o tipo pelo sintoma encurta muito a depuração, porque cada um aponta para uma categoria distinta de engano.