Por que erros são inevitáveis
Todo programa eventualmente encontra uma situação inesperada: um servidor retorna um 500, um usuário digita letras em um campo numérico, um arquivo está faltando ou uma API de terceiros muda sua estrutura. O tratamento de erros não serve para evitar todas as falhas — mas sim para decidir o que acontece quando uma delas ocorre.
Erros bem tratados mantêm a aplicação funcionando, informam algo útil ao usuário e fornecem aos desenvolvedores os detalhes necessários. Erros mal tratados resultam em telas em branco, perda silenciosa de dados e relatórios de bugs impossíveis de reproduzir.
O objeto Error
O JavaScript possui um tipo Error nativo, e todo erro lançado geralmente é uma instância dele.
// error.js
const error = new Error("Something broke");
error.message; // "Something broke"
error.name; // "Error"
error.stack; // the call stack at the point of creation
error.cause; // an optional underlying error
Existem diversas subclasses nativas para categorias comuns: TypeError, ReferenceError, RangeError, SyntaxError, URIError e EvalError. Elas diferem principalmente no nome e nas situações que as produzem, o que as torna úteis para depuração e para criar ramificações baseadas no tipo de falha.
Lançando erros
Use throw para sinalizar que uma regra foi violada. Lance um objeto Error, nunca uma string ou um número — apenas instâncias de Error carregam uma mensagem, um nome e um stack trace.
// validate.js
function divide(a, b) {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
Lançar erros precocemente, no ponto exato onde uma premissa é quebrada, evita que estados inválidos se propaguem profundamente em seu programa. Falhar ruidosamente durante o desenvolvimento é um recurso, não um incômodo.
try, catch e finally
O try executa código arriscado. Se algo lançar um erro, o controle salta para o catch. O finally é executado de qualquer maneira, o que o torna o lugar ideal para a limpeza (cleanup).
// try.js
try {
const data = JSON.parse(input);
save(data);
} catch (error) {
console.error("Invalid data:", error.message);
} finally {
setLoading(false);
}
O parâmetro catch é o valor lançado. O JavaScript moderno também suporta optional catch binding quando você não precisa dele: catch { ... }. Dentro de um bloco catch, você pode inspecionar o erro, registrá-lo em logs, recuperar a execução ou relançá-lo caso não consiga tratá-lo.
// rethrow.js
try {
await loadProfile();
} catch (error) {
log(error);
throw error; // let a higher layer decide
}
Classes de erro customizadas
Erros genéricos perdem o sentido à medida que sua aplicação cresce. Classes customizadas permitem que você anexe contexto e crie ramificações baseadas no tipo de falha.
// http-error.js
class HttpError extends Error {
constructor(status, message) {
super(message ?? `HTTP ${status}`);
this.name = "HttpError";
this.status = status;
}
}
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
Agora, quem chama a função pode reagir com precisão:
// handle.js
try {
await save(form);
} catch (error) {
if (error instanceof ValidationError) {
showFieldError(error.field, error.message);
} else if (error instanceof HttpError && error.status === 401) {
redirectToLogin();
} else {
showGenericError();
}
}
Esta é a diferença entre “algo deu errado” e “o campo de e-mail já está sendo utilizado”.
Envolvendo erros com cause
Às vezes, você deseja adicionar contexto sem perder a falha original. A opção cause preserva a cadeia de erros.
// cause.js
try {
await db.query(sql);
} catch (error) {
throw new Error("Failed to load orders", { cause: error });
}
O erro externo carrega o contexto amigável para o usuário, enquanto o error.cause mantém os detalhes originais para logs e depuração.
Erros em código assíncrono
O código assíncrono possui dois caminhos de falha adicionais. Com async/await, try/catch funciona exatamente como no código síncrono.
// async.js
async function load() {
try {
const res = await fetch("/api/data");
if (!res.ok) throw new HttpError(res.status);
return await res.json();
} catch (error) {
console.error("Load failed:", error);
throw error;
}
}
Sem await, anexe um handler à promise:
// promise.js
fetch("/api/data")
.then((res) => res.json())
.catch((error) => console.error(error));
Uma promise rejeitada sem um handler torna-se uma unhandled rejection. Nos navegadores, isso gera um aviso no log; no Node.js, isso pode encerrar o processo. Sempre trate as rejeições, mesmo que seja apenas para registrar o log e relançar o erro.
Redes de segurança globais
Mesmo com um código cuidadoso, algo acabará passando. Handlers globais capturam o que você deixou escapar para que o app possa reportar o erro em vez de morrer silenciosamente.
// global.js
window.addEventListener("error", (event) => {
reportToServer(event.error);
});
window.addEventListener("unhandledrejection", (event) => {
reportToServer(event.reason);
});
No Node.js, os equivalentes são process.on("uncaughtException") e process.on("unhandledRejection"). Trate-os como último recurso para logging e shutdown gracioso, e não como um substituto para o tratamento de erros onde eles ocorrem.
Valide antes de lançar
Nem todo problema é excepcional. Condições esperadas — entrada vazia, um campo opcional ausente, uma busca sem resultados — são melhor tratadas com validação do que com exceções.
// guards.js
function formatName(user) {
if (!user?.name) return "Anonymous";
return user.name.trim();
}
Reserve as exceções para situações genuinamente inesperadas ou irrecuperáveis. Usá-las para o fluxo de controle comum torna o código mais lento e difícil de acompanhar.
Falando com os usuários
Uma mensagem de erro técnica é voltada para desenvolvedores. Os usuários precisam saber o que aconteceu e o que podem fazer a seguir.
- Mantenha as mensagens curtas e humanas: “Não foi possível salvar suas alterações. Verifique sua conexão e tente novamente.”
- Evite jargões, stack traces e códigos de status brutos na interface.
- Forneça um próximo passo: tentar novamente, voltar, entrar em contato com o suporte ou continuar offline.
- Registre todos os detalhes — mensagem, stack, contexto — para que o suporte possa investigar.
Melhores práticas
- Lance objetos
Error, nunca strings. - Use o catch apenas onde for possível recuperar o erro ou adicionar contexto útil.
- Utilize classes de erro customizadas para falhas específicas do domínio.
- Envolva erros com
causeem vez de descartar o original. - Sempre trate as rejeições de promises.
- Adicione handlers globais como uma rede de segurança final e envie os logs para um serviço real.
- Valide as condições esperadas em vez de apenas lançar erros.
- Mantenha as mensagens para o usuário separadas dos diagnósticos para o desenvolvedor.
- Escreva testes para os caminhos de falha, não apenas para o “happy path”.
Erros comuns
- “Engolir” erros com um bloco
catchvazio. - Lançar strings e perder o stack trace.
- Capturar um erro e retornar um valor padrão que esconde um bug real.
- Esquecer de tratar promises rejeitadas.
- Exibir mensagens de erro brutas para os usuários.
- Usar exceções para fluxo de controle comum.
- Assumir que
try/catchao redor de um executor de promise captura erros assíncronos — isso não acontece.
Próximos passos
Os erros são onde o código assíncrono e a Fetch API encontram a realidade. Combine os padrões apresentados aqui com o DOM para renderizar estados amigáveis ao usuário, e revise os fundamentos de JavaScript sempre que um TypeError lembrar você que undefined não é uma função.