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

Tratamento de erros em requisições HTTP
Tratamento de erros em requisições HTTP

Código que só funciona quando tudo dá certo não está pronto para produção…

Deploy: do código ao ar
Deploy: do código ao ar

Construir é metade do trabalho; a outra metade é colocar no ar. O artigo…

Objetos: estruturando dados do mundo real
Objetos: estruturando dados do mundo real

Atribuir um objeto a outra variável não copia coisa nenhuma: as duas passam a…