Tratamento unificado de exceções em JavaScript
Em aplicações JavaScript modernas, especialmente em projetos maiores ou com múltiplas camadas (como back-end e front-end), é comum que o tratamento de exceções se torne repetitivo e fragmentado. Blocos try...catch
aninhados dificultam a leitura, aumentam o acoplamento e dificultam a manutenção.
Uma abordagem mais limpa e previsível consiste em adotar um padrão unificado para lidar com erros, encapsulando chamadas em funções auxiliares que retornam resultados estruturados. Esse padrão simplifica o fluxo de controle, melhora a depuração e favorece a escrita de código mais funcional e reutilizável.
Encapsulando exceções com tryCatch
A função tryCatch
executa qualquer função passada como argumento e captura automaticamente erros, retornando um array no formato [resultado, erro]
:
function tryCatch(fn) {
try {
return [fn(), null];
} catch (err) {
return [null, err];
}
}
Se a execução for bem-sucedida, o erro será null
. Caso contrário, o resultado será null e o erro capturado poderá ser tratado externamente, sem a necessidade de blocos try...catch
explícitos em cada chamada.
Exemplo: cálculo de raiz quadrada
A função sqrt
abaixo calcula a raiz quadrada de um número, mas lança um erro se o valor for negativo:
const sqrt = (x) => {
if (x < 0) throw new Error(`Number ${x} is invalid`);
return Math.sqrt(x);
};
Com o wrapper:
const [res1, err1] = tryCatch(() => sqrt(-4));
if (err1) console.error(err1);
else console.log("sqrt(-4):", res1);
const [res2, err2] = tryCatch(() => sqrt(4));
if (err2) console.error(err2);
else console.log("sqrt(4):", res2);
No primeiro caso, sqrt(-4)
gera uma exceção que é capturada. No segundo, o resultado é 2, sem erro.
Aplicando a construtores
A mesma estratégia pode ser aplicada ao instanciamento de classes. Veja um exemplo com uma classe Person
:
class Person {
constructor(name, age) {
if (age < 0) throw `Idade inválida ${age}. Valor mínimo: 1`;
if (name.length < 2) throw `Nome inválido ${name}: mínimo de 2 caracteres`;
this.name = name;
this.age = age;
}
print() {
console.log(`Name: ${this.name} Age: ${this.age}`);
}
}
Instanciando com o wrapper:
const [tom, err1] = tryCatch(() => new Person("Tom", -123));
if (err1) console.error(err1);
else tom.print();
const [bob, err2] = tryCatch(() => new Person("Bob", 46));
if (err2) console.error(err2);
else bob.print();
O primeiro construtor lança erro ao receber idade negativa, e o segundo instancia normalmente.
Garantindo consistência no tipo de erro
Como JavaScript permite lançar qualquer tipo de valor como erro (não apenas instâncias de Error
), é possível adaptar tryCatch
para converter qualquer valor em um erro padrão:
function tryCatch(fn) {
try {
return [fn(), null];
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
return [null, error];
}
}
Isso padroniza os erros, facilitando o tratamento e o monitoramento.
Versão reutilizável com argumentos
Uma variação útil da função tryCatch
é um wrapper reutilizável, que retorna uma nova função com tratamento interno de exceções. Essa abordagem permite reaproveitar a lógica de captura de erros em múltiplas chamadas, sem precisar embrulhar cada execução manualmente.
const tryWrap =
(fn) =>
(...args) => {
try {
return [fn(...args), null];
} catch (err) {
return [null, err];
}
};
Esse wrapper recebe a função original como argumento e devolve outra função que aceita quaisquer parâmetros e executa a chamada original com tratamento de erro. O retorno segue o mesmo padrão: [resultado, erro]
.
tryWrap
devolve a função já protegida, pronta para ser chamada diretamente:
const safeSqrt = tryWrap(sqrt);
const [res1, err1] = safeSqrt(-4);
const [res2, err2] = safeSqrt(16);
Quando a função recebe vários argumentos, o ganho de clareza fica mais evidente:
const safePerson = tryWrap((name, age) => new Person(name, age));
const [tom, tomErr] = safePerson("Tom", -123);
const [bob, bobErr] = safePerson("Bob", 46);
A função original continua inalterada, mas agora existe uma forma segura e reaproveitável de chamá-la.
Retorno estruturado nativamente
Outra opção é fazer com que a própria função já siga o contrato de retorno [resultado, erro]
, sem depender de wrappers externos:
const sqrt = (x) => {
if (x < 0) return [null, new Error(`Number ${x} is invalid`)];
return [Math.sqrt(x), null];
};
Com esse padrão, o código se torna ainda mais direto:
const [res1, err1] = sqrt(-4);
if (err1) console.error(err1);
else console.log("sqrt(-4):", res1);
const [res2, err2] = sqrt(4);
if (err2) console.error(err2);
else console.log("sqrt(4):", res2);
Conclusão
O uso de wrappers ou contratos de retorno padronizados oferece uma forma mais limpa, segura e previsível de lidar com exceções em JavaScript. Essa abordagem reduz a necessidade de blocos try...catch
dispersos, melhora a legibilidade, facilita testes e torna o código mais compatível com estilos funcionais e ambientes complexos.
O mesmo padrão pode ser adaptado para funções assíncronas, com versões compatíveis com async/await
, mantendo a consistência no tratamento de exceções em toda a base do projeto.