Tratamento de Erros com try, catch e finally

[39] Tratamento de Erros com try, catch e finally

Um catch vazio transforma falha em silêncio: a tela não reage, o log não registra, e o usuário tenta de novo gerando pedido duplicado. Contra isso, o artigo monta try, catch e finally, o throw dos seus próprios erros, as classes que estendem Error, e a ordem de verificação que não pode começar pelo genérico.
Javascript

16 min de leitura

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:

  1. Lance um erro se notas não for um array
  2. Lance um erro se o array estiver vazio
  3. Lance um erro se alguma nota não for um número entre 0 e 10
  4. Retorne a média calculada se tudo estiver correto
  5. 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

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.

Comentários

Mais em Javascript

Mini Projeto: App de Clima Completo
Mini Projeto: App de Clima Completo

O Módulo 3 fecha juntando tudo num app de clima: geolocalização do navegador…

MongoDB e Mongoose: banco de dados com Node
MongoDB e Mongoose: banco de dados com Node

Um array na memória some quando o servidor reinicia, e é aí que entra o banco…

Revisão + Projeto Final: SPA Completa
Revisão + Projeto Final: SPA Completa

As cinco peças do módulo em uma aplicação só: rotas com layout e proteção…