Existem muito poucos programas que funcionam de forma isolada. Mesmo que os desenvolvedores sempre escrevam o código perfeito, há uma grande probabilidade de encontrar erros quando o código interage com componentes externos como bancos de dados, APIs REST e até mesmo aquele pacote npm da moda que tem um monte de estrelas!

Em vez de sentado enquanto o servidor trava, um desenvolvedor responsável pensa defensivamente e se prepara para quando uma solicitação malformada chega. Neste artigo, abordaremos algumas das abordagens mais comuns para tratamento de erros no TypeScript. Vamos aprender como cada método é usado, ver como cada método pode ser melhorado e, finalmente, propor uma maneira mais limpa de gerenciar erros!

Vamos começar!

Observação: se você não estiver familiarizado com o TypeScript, boas notícias! As condições que causam erros e as soluções também se aplicam ao JavaScript.

Abordagens populares de tratamento de erros do TypeScript

Antes de nos aprofundarmos, tenha em mente que a lista a seguir é por não significa exaustivo! Os erros e soluções apresentados aqui são baseados em minha experiência subjetiva, portanto, sua milhagem pode variar!

Retornando nulo

Retornando nulo é um maneira comum de indicar que algo deu errado em seu código. É melhor usado quando uma função tem apenas uma maneira de falhar, no entanto, alguns desenvolvedores a usam quando uma função tem muitos erros.

Retornar nulo força verificações nulas em todo o seu código, causando informações específicas sobre a causa do erro a ser perdido. Retornar nulo é uma representação arbitrária de um erro, então se você tentar retornar 0,-1 ou falso, você terminará com o mesmo resultado.

No bloco de código abaixo, escreveremos uma função que recupera dados sobre a temperatura e umidade de uma determinada cidade. A função getWeather interage com duas APIs externas por meio de duas funções, externalTemperatureAPI e externalHumidityAPI, e agrega os resultados:

const getWeather=async (city: string): Promise <{temp: number; umidade: número} | null>=> {const temp=await externalTemperatureAPI (cidade); if (! temp) {console.log (`Erro ao buscar temperatura para $ {city}`); return null; } umidade constante=espera externalHumidityAPI (cidade); if (! umidade) {console.log (`Erro ao buscar umidade para $ {city}`); return null; } return {temp, umidade}; };//… const weather=await getWeather (“Berlin”); if (weather===null) console.log (“getWeather () falhou”);

Podemos ver que, ao entrar em Berlim, recebemos as mensagens de erro Erro ao buscar temperatura para $ {cidade} e Erro ao buscar umidade para $ {cidade}.

Ambas as funções externas da API podem falhar, então getWeather é forçado a verificar se há null para ambas as funções. Embora a verificação de null seja melhor do que não manipular erros, ela força o chamador a fazer algumas suposições. Se uma função for estendida para oferecer suporte a um novo erro, o chamador não saberá, a menos que verifique os internos da função.

Digamos que externalTemperatureAPI inicialmente lance um nulo quando a API de temperatura retornar o código HTTP 500 , o que indica um erro interno do servidor. Se estendermos nossa função para verificar a validade estrutural e de tipo da resposta da API (ou seja, verificar se a resposta é do tipo número), o chamador não saberá se a função retorna nulo devido ao código HTTP 500 ou uma estrutura de resposta inesperada da API.

Lançar erros personalizados usando try… catch

Criar erros personalizados e jogá-los é uma opção melhor do que retornar nulo porque podemos obter granularidade de erro, o que permite que uma função lance erros distintos e permite que o chamador da função trate os erros distintos separadamente.

No entanto, qualquer função que lance um erro será interrompida e propagada, interrompendo o fluxo regular do código. Embora isso possa não parecer grande coisa, especialmente para aplicativos menores, à medida que seu código continua com a camada de tentativa… captura após tentativa… captura, a legibilidade e o desempenho geral diminuirão.

Vamos tentar resolva o erro em nosso exemplo de clima com o método try… catch:

const getWeather=async (city: string): Promise <{temp: number; umidade: número}>=> {try {const temp=await externalTemperatureAPI (cidade); tente {const umidade=aguarde externalHumidityAPI (cidade); } catch (error) {console.log (`Erro ao buscar umidade para $ {city}`); retornar um novo erro (`Erro ao buscar umidade para $ {city}`); } return {temp, umidade}; } catch (error) {console.log (`Erro ao buscar temperatura para $ {city}`); retornar um novo erro (`Erro ao buscar temperatura para $ {city}`); }};//… tente {const weather=await getWeather (“Berlin”); } catch (erro) {console.log (“getWeather () falhou”); }

No bloco de código acima, quando tentamos acessar o externalTemperatureAPI e o externalHumidityAPI, encontramos dois erros no console.log, que são interrompidos e propagados várias vezes.

A classe Result

Quando você usa qualquer uma das duas abordagens de tratamento de erros discutidas acima, um erro simples pode adicionar complexidade desnecessária ao erro original. Os problemas que surgem ao retornar null e lançar um try… catch são comuns em outras linguagens de front-end como Kotlin , Rust e C #. Essas três linguagens usam a classe Result como uma solução bastante generalizada.

Independentemente de a execução ser bem-sucedida ou falhar, a classe Result encapsulará os resultados da função dada, permitindo que os chamadores de função tratem os erros como parte de o fluxo de execução normal em vez de uma exceção.

Quando emparelhado com TypeScript, a classe Result fornece segurança de tipo e informações detalhadas sobre os possíveis erros que uma função pode resultar. Quando modificamos os resultados de erro de uma função, a classe Result nos fornece erros em tempo de compilação em os locais afetados de nossa base de código.

Vamos voltar ao nosso exemplo de clima. Usaremos uma implementação TypeScript dos objetos Result e Option do Rust, ts-results :

Existem outros pacotes para TypeScript com APIs muito semelhantes, como NeverThrow , então você deve se sentir à vontade para brincar!

import {Ok, Err, Result} de”ts-results”; tipo Erros=”CANT_FETCH_TEMPERATURE”|”CANT_FETCH_HUMIDITY”; const getWeather=async (city: string): Promise >=> {const temp=await externalTemperatureAPI (cidade); if (! temp) return Err (“CANT_FETCH_TEMPERATURE”); umidade constante=espera umidade externaAPI (cidade); if (! umidade) return Err (“CANT_FETCH_HUMIDITY”); retornar Ok ({temp, umidade}); };//… const weatherResult=await getWeather (“Berlin”);//weatherResult é totalmente digitado if (weatherResult.err) console.log (`getWeather () falhou: $ {weatherResult.val}`); if (weatherResult.ok) console.log (`O clima é: $ {JSON.stringify (weather.val)}`);

Adicionar resultados de tipo seguro da função e priorizar o tratamento de erros em nosso código é uma melhoria em relação aos nossos exemplos anteriores, no entanto, ainda temos trabalho a fazer! Vamos explorar como podemos tornar nossas verificações de tipo exaustivas.

É importante observar que favorecer a classe Result não significa que você não usará estruturas try… catch! As estruturas try… catch ainda são necessárias quando você está trabalhando com pacotes externos.

Se você acha que vale a pena seguir a classe Result, você pode tentar encapsular esses pontos de contato em módulos e usar a classe Result internamente.

Adicionando verificação de tipo exaustiva

Ao lidar com funções que podem retornar vários erros, pode ser útil fornecer verificações de tipo para cobrir todos os casos de erro. Isso garante que o chamador da função possa reagir dinamicamente ao tipo de erro e fornece a certeza de que nenhum caso de erro será esquecido.

Podemos fazer isso com uma opção exaustiva:

//Exaustivo switch helper class UnreachableCaseError estende Error {constructor (val: never) {super (`Unreachable case: $ {val}`); }}//… const weatherResult=getWeather (“Berlin”); if (weatherResult.err) {//lida com erros const errValue=weatherResult.val; switch (errValue) {case”CANT_FETCH_TEMPERATURE”: console.error (“getWeather () falhou com: CANT_FETCH_TEMPERATURE”); quebrar; case”CANT_FETCH_HUMIDITY”: console.error (“getWeather () falhou com: CANT_FETCH_HUMIDITY”); quebrar; padrão: lançar novo UnreachableCaseError (errValue);//verificação de tipo de tempo de execução para capturar todos os erros}}

Executar nosso exemplo de clima com a opção exaustiva fornecerá erros de tempo de compilação em dois conjuntos de circunstâncias. Um é quando todos os casos de erro não são tratados e o outro é quando os erros na função original mudam.

Resumo

Agora, você conhece uma solução aprimorada para lidar com erros comuns em TypeScript! Sabendo a importância do tratamento de erros, espero que você use este método para obter as informações mais específicas sobre quaisquer erros em seu aplicativo.

Neste tutorial, cobrimos as desvantagens de algumas abordagens amplamente difundidas, como retornar null e o método try… catch. Por fim, aprendemos como usar a classe TypeScript Result com uma opção exaustiva para detecção de erros.