O uso de try/catch é o padrão mais comum para lidar com erros no Javascript.
Entretanto, essa abordagem possui algumas desvantagens. Não existe uma forma de verificar
se uma função possivelmente faz um throw. Também não é possível verificar
quais os erros que o catch vai receber. Isso tudo favorece um código que geralmente
inclui grandes blocos de try/catch, onde múltiplos erros são tratados de forma igual, e muitas informações são perdidas.
Além disso, o uso de throw prejudica o entendimento do código porque muda o fluxo do código. Nesse caso, para entender qual código vai ser executado depois do erro ser lançado, é necessário navegar por todo o código em busca do catch mais próximo, o que pode ser difícil e confuso.
async function getUser(id: string) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
// Erro 1: A resposta HTTP indica falha (ex: 404, 500)
throw new Error(`Request failed: ${response.status}`);
}
// Erro 2: O corpo da resposta não é um JSON válido
const data = await response.json();
return data;
} catch (error) {
// Erro 3: Erro de rede (ex: DNS falhou, sem internet)
// Aqui, 'error' mistura os três cenários acima.
// É difícil saber programaticamente qual falha ocorreu e como reagir
// especificamente a cada uma delas sem muita verificação de tipos.
console.error("Failed to fetch user", error);
}
}
Por que o erro do catch é sempre unknown?
No JavaScript, o throw permite lançar qualquer tipo de valor, não apenas Error. É perfeitamente válido escrever throw "deu ruim", throw 404, ou até mesmo throw null.
Devido a essa flexibilidade, o TypeScript adota uma postura segura e tipa a variável de erro no bloco catch como unknown. Isso força o desenvolvedor a verificar o tipo do erro (usando typeof ou instanceof) antes de tentar acessar propriedades como .message ou .stack, garantindo que o valor existe e é do tipo esperado.
Uma alternativa possível é o padrão de utilizar Erro como valor. Essa abordagem é utilizada em algumas linguagens como Go, Rust e recentemente Kotlin. A ideia é que as funções retornem os erros como valores, deixando claro para o consumidor daquela função quais são os possíveis erros.
Esse padrão pode ser simulado no typescript com o tipo Result:
type Result<T, E> =
| {
ok: true;
value: T;
}
| {
ok: false;
error: E;
};
O tipo Result representa a resposta de uma função sobre dois genéricos, o T, que representa
os dados retornados, e E que representa um erro. Além disso, possui uma propriedade discriminatória ok, que
indica de que se trata de um erro ou de um sucesso.
// Definindo os possíveis erros
type NetworkError = { _tag: "NetworkError"; message: string };
type NotFoundError = { _tag: "NotFoundError"; userId: string };
type ParseError = { _tag: "ParseError"; message: string };
type FetchUserError = NetworkError | NotFoundError | ParseError;
// Função que retorna Result
async function fetchUser(id: string): Promise<Result<User, FetchUserError>> {
try {
const response = await fetch(`/api/users/${id}`);
if (response.status === 404) {
return { ok: false, error: { _tag: "NotFoundError", userId: id } };
}
if (!response.ok) {
return {
ok: false,
error: {
_tag: "NetworkError",
message: `HTTP ${response.status}`,
},
};
}
const data = await response.json();
return { ok: true, value: data };
} catch (error) {
return {
ok: false,
error: {
_tag: "ParseError",
message: "Failed to parse response",
},
};
}
}
// Consumindo a função
const userResult = await fetchUser("123");
if (userResult.ok) {
console.log("User found:", userResult.value);
// TypeScript sabe que userResult.value é do tipo User
} else {
// TypeScript sabe que userResult.error é FetchUserError
switch (userResult.error._tag) {
case "NotFoundError":
console.error(`User ${userResult.error.userId} not found`);
break;
case "NetworkError":
console.error(`Network error: ${userResult.error.message}`);
break;
case "ParseError":
console.error(`Parse error: ${userResult.error.message}`);
break;
}
}
O objetivo não é eliminar completamente o uso do throw/catch,
mas sim reservá-lo para situações de falha extrema, situações
em que a execução deve ser parada imediatamente.
Por exemplo, quando uma variável de ambiente obrigatória não está presente na inicialização,
ou quando a conexão com o banco de dados falha irremediavelmente durante o boot.
Assim, temos uma separação entre erros esperados (usuário não encontrado, saldo insuficiente, falha de validação) e erros não esperados (estouro de memória, erros de sintaxe SQL). Além disso, podemos adicionar metadados aos erros esperados, que facilitam debugação e observabilidade do sistema.
Embora seja possível definir seu próprio tipo Result e funções auxiliares manualmente, para aplicações reais é recomendado utilizar uma solução com mais recursos. A partir daqui, utilizaremos a biblioteca typescript-result.
A seguir, discutiremos três padrões distintos para tratar melhor os erros nas suas aplicações.
Criando um Result
Antes de poder consumir um Result, nós precisamos adaptar nossas funções para gerar um Result.
O typescript-result expõe algumas APIs para isso.
Result.ok e Result.error geram, respectivamente, um resultado de uma operação bem sucedida e um resultado de um erro.
import { Result } from "typescript-result";
type DiscountError =
| { _tag: "NegativeAmount"; amount: number }
| { _tag: "ExceedsMaximum"; amount: number; max: number };
function calculateDiscountPercentage(
amount: number,
): Result<number, DiscountError> {
if (amount <= 0) {
return Result.error({ _tag: "NegativeAmount", amount });
}
if (amount > 100) {
return Result.error({ _tag: "ExceedsMaximum", amount, max: 100 });
}
const finalValue = amount * 0.9; // 10% de desconto
return Result.ok(finalValue);
}
const result = calculateDiscountPercentage(30);
// result: Result<number, DiscountError>
Você pode explicitamente definir o retorno como Result<number, DomainError>, por exemplo.
Apesar do TypeScript inferir os tipos automaticamente, definir o retorno explicitamente é considerado uma boa prática se você está criando uma função pública (biblioteca ou API), pois garante que o contrato da função seja respeitado e evita que tipos internos "vazem" acidentalmente.
Para facilitar a geração de Results a partir de funções que podem fazer um throw,
você pode utilizar o try ou wrap.
import { Result } from "typescript-result";
function divide(a: number, b: number) {
if (b === 0) throw new Error("Cannot divide by zero");
return a / b;
}
// Result.try executa a função imediatamente e captura exceções
const immediateResult = Result.try(() => divide(10, 0));
// immediateResult: Result<number, Error>
// Com transformação de erro (opcional)
const taggedResult = Result.try(
() => divide(10, 0),
(error) => ({ _tag: "MathError", message: String(error) }),
);
// Result.wrap retorna uma nova função que sempre retorna Result
// Útil para converter funções existentes
const safeDivide = Result.wrap(divide);
const result = safeDivide(10, 0);
// result: Result<number, Error>
Por que os erros possuem a propriedade _tag?
Typescript possui verificação estrutural, o que significa que, para fins de verificação, os seguintes erros são os mesmos:
class ValidationError {
constructor(public message: string) {}
}
class DatabaseError {
constructor(public message: string) {}
}
// TypeScript considera esses tipos compatíveis!
const error1: ValidationError = new DatabaseError("Connection failed");
const error2: DatabaseError = new ValidationError("Invalid input");
Em alguns casos, o Typescript pode "colapsar" os tipos em um só, quando na verdade eles possuem significados semânticos diferentes.
import { Result } from "typescript-result";
function readData(name: string) {
if (name === "db") {
return Result.error(new DatabaseError("Connection failed"));
}
if (name === "invalid") {
return Result.error(new ValidationError("Invalid data"));
}
return Result.ok({ data: `Data for ${name}` });
}
const result = readData("something");
// ^? Result.Error<DatabaseError> | Result.Ok<{ data: string; }>
// Typescript não infere o possível erro `ValidationError`
Você pode ler mais sobre a verificação estrutural do typescript no meu artigo: Entendendo Structural Typing no TypeScript.
Além disso, ter uma propriedade como _tag facilita a distinção dos erros em runtime,
evitando o uso de instanceof. Em aplicações modernas (grandes monorepos, micro-frontends, etc), o instanceof pode falhar. Isso acontece porque a classe instanciada pode vir de um contexto ou bundle diferente da classe que você está usando para a comparação, mesmo que elas tenham o mesmo nome e código. Uma string literal (_tag) é imune a esse problema.
Essas funções utilitárias são fundamentais quando precisamos interagir com bibliotecas externas ou código legado que não utilizam o padrão Result. Elas funcionam como uma barreira de proteção nas bordas da sua aplicação, encapsulando exceções imprevisíveis e trazendo-as para dentro do fluxo controlado e tipado do Result.
Até agora, vimos exemplos que retornam Promise<Result<T, E>>. Entretanto, a biblioteca typescript-result fornece um tipo especializado chamado AsyncResult<T, E>, que é essencialmente um alias para Promise<Result<T, E>>.
// Ao invés de Promise<Result<T, E>>
async function fetchUser(id: string): AsyncResult<User, FetchUserError> {
// Mesma implementação
}
Verificação Manual de Erros
Agora, quando utilizamos uma função que retorna um erro, precisamos verificar se não temos um erro. Podemos tomar uma decisão com o que fazer com aquele erro, ou repassar ele para cima:
type ValidationError = { _tag: "ValidationError"; field: string };
type DatabaseError = { _tag: "DatabaseError"; message: string };
type NotFoundError = { _tag: "NotFoundError"; id: string };
async function processOrder(
orderId: string,
): AsyncResult<Order, ValidationError | DatabaseError | NotFoundError> {
// Buscar o pedido
const orderResult = await fetchOrder(orderId);
if (!orderResult.ok) {
// Propaga o erro para cima
return orderResult;
}
const order = orderResult.value;
// Validar o pedido
const validationResult = validateOrder(order);
if (!validationResult.ok) {
// Trata erro específico ou propaga
if (validationResult.error._tag === "ValidationError") {
console.error(`Validation failed: ${validationResult.error.field}`);
}
return validationResult;
}
// Salvar no banco
const saveResult = await saveToDatabase(order);
if (!saveResult.ok) {
// Propaga erro de banco de dados
return saveResult;
}
// Sucesso! Retorna o pedido processado
return Result.ok(saveResult.value);
}
Esse padrão é muito semelhante ao padrão utilizado em Go,
onde funções, por convenção, retornam sempre um possível resultado
e um possível erro. Em Go, em vez de usar throw/catch, as funções retornam
múltiplos valores: o resultado esperado e um erro (se houver). É como se JavaScript tivesse:
[resultado, erro] = await minhaFuncao() em cada chamada.
func processOrder(orderId string) (*Order, error) {
// Em Go, a função retorna dois valores: order e err
// Se err é diferente de nil, significa que houve um erro
order, err := fetchOrder(orderId)
if err != nil {
// Semelhante ao "se o erro existe, retorna para o chamador"
return nil, err
}
// Validar o pedido
// Novamente, verificamos se houve erro nessa operação
err = validateOrder(order)
if err != nil {
// Registra o erro e propaga para o chamador
log.Printf("Validation failed: %v", err)
return nil, err
}
// Salvar no banco
// Go força a sempre verificar se um erro ocorreu
savedOrder, err := saveToDatabase(order)
if err != nil {
// Erro de banco de dados, propaga para o chamador
return nil, err
}
// Sucesso! Retorna o pedido e nil (sem erro)
return savedOrder, nil
}
Uma das vantagens dessa abordagem é que ela é extremamente simples de entender, mesmo se alguém não estiver familiarizado com essa biblioteca ou padrão. Ele deixa explícito os pontos de tomada de decisão.
Entretanto, um problema é a verbosidade. Por exemplo, um erro precisa se propagar (bubble up) até atingir a camada adequada que vai lidar com ele.
Encadeando operações (Chaining)
E se, em vez de verificar o conteúdo de um Result, a gente tivesse um método que aplicasse uma função no Result somente se ele fosse um sucesso, retornando o result imediatamente se ele na verdade for um erro?
Toda instância de Result possui alguns métodos que permitem trabalhar com o conteúdo de um Result,
sem a necessidade de verificar seu conteúdo.
O .map() aplica uma função somente se o resultado corresponder com um sucesso (que pode retornar um valor, ou até mesmo um outro Result), enquanto que o .mapError() aplica a função somente se o resultado for um erro. Note que essas funções retornam outro Result,
o que possibilita encadear multiplas chamadas.
async function getUserEmail(
userId: string,
): AsyncResult<string, FetchUserError> {
return fetchUser(userId)
.map((user) => user.email) // Só executa se fetchUser retornar sucesso
.map((email) => email.toLowerCase()); // Encadeia transformações
}
// Exemplo com mapError - normalizando erros
async function processUserData(userId: string): AsyncResult<User, AppError> {
return fetchUser(userId).mapError((error) => {
// Transforma erros específicos em erro genérico da aplicação
switch (error._tag) {
case "NetworkError":
return { _tag: "AppError" as const, message: "Serviço indisponível" };
case "NotFoundError":
return { _tag: "AppError" as const, message: "Usuário não encontrado" };
case "ParseError":
return { _tag: "AppError" as const, message: "Dados inválidos" };
}
});
}
// Ambos podem ser encadeados
const result = await fetchUser("123")
.map((user) => ({ ...user, isActive: true }))
.mapError((error) => ({ ...error, timestamp: Date.now() }));
Se você deseja observar o valor sem modificá-lo (efeitos colaterais), pode utilizar os métodos onSuccess ou onFailure.
Um uso comum é para adicionar observabilidade e logs:
type LoginError =
| { _tag: "InvalidCredentials" }
| { _tag: "AccountLocked"; reason: string }
| { _tag: "NetworkError"; message: string };
async function loginUser(
email: string,
password: string,
): AsyncResult<User, LoginError> {
return authenticateUser(email, password)
.onSuccess((user) => {
// Side-effect: Log de sucesso
console.log(
`Login bem-sucedido para ${user.email} às ${new Date().toISOString()}`,
);
// Poderia enviar para serviço de analytics
analytics.track("user_login", { userId: user.id, timestamp: Date.now() });
})
.onFailure((error) => {
// Side-effect: Log de erro
console.error(`Falha no login: ${error._tag}`);
if (error._tag === "InvalidCredentials") {
console.warn(`Tentativa de login com credenciais inválidas`);
} else if (error._tag === "AccountLocked") {
console.error(`Conta bloqueada: ${error.reason}`);
}
});
}
// O valor original do Result não é alterado, apenas observado
const result = await loginUser("user@example.com", "password");
// result continua sendo Result<User, LoginError>
Esse padrão também é conhecido como Railway Oriented Programming (ROP), porque temos duas "trilhas", a de sucesso e de falha, e aplicamos funções em trilhas diferentes. O caminho comum é focar na trilha do sucesso (happy path), direcionando para a outra trilha quando encontramos um erro.
Entretanto, em alguns casos, você pode
querer fazer o caminho inverso, uma função que, baseada em um erro, tenta "voltar" para a trilha de sucessos. Para esse
caso, temos a função .recover()
type ApiError = { _tag: "PrimaryApiDown" } | { _tag: "SecondaryApiDown" };
// Funções que fazem requisições para diferentes APIs
async function fetchFromPrimaryApi(id: string): AsyncResult<Data, ApiError> {
// ...
}
async function fetchFromSecondaryApi(id: string): AsyncResult<Data, ApiError> {
// ...
}
// Usando .recover para fallback entre APIs
async function fetchDataWithFallback(id: string): AsyncResult<Data, ApiError> {
return fetchFromPrimaryApi(id).recover(() => fetchFromSecondaryApi(id));
}
Esse padrão de encadeamento (chaining) é muito comum na programação funcional, tratando o Result como um monad.
Esse padrão é semelhante ao uso de pipelines, e deixa explícita as etapas. Em Rust, existe um padrão muito semelhante:
fn process_user_data(id: &str) -> Result<String, AppError> {
// Em Rust, Result é nativo da linguagem e possui os mesmos métodos
fetch_user(id)
// |user| é uma closure (equivalente a arrow functions)
// Em Javascript, seria (user) => user.email
.map(|user| user.email)
.map(|email| email.to_lowercase())
.map_err(|err| AppError::from(err))
}
Entretanto, ele pode fugir do formato típico de funções do Javascript/Typescript. Ele também pode causar problemas de legibilidade quando todas as lógicas precisam estar implementadas em funções.
Usando Generators
O Result também fornece uma outra forma de gerar um Result, com um generator:
// Exemplo simples com Result.gen()
function calculateTotal(items: Item[]): Result<number, ValidationError> {
return Result.gen(function* () {
// Valida cada item e extrai seu valor
const validatedItems = yield* validateItems(items);
const subtotal = yield* calculateSubtotal(validatedItems);
const discount = yield* applyDiscount(subtotal);
return subtotal - discount;
});
}
Generators permitem uma forma de inversão de controle (IoC). O Result.gen() recebe o generator que você escreveu e o executa. Para cada yield*, ele verifica o Result: se houver um erro, a execução para imediatamente e retorna o erro. Caso contrário, ele extrai o valor de sucesso e passa esse valor de volta para o generator, permitindo que a execução continue. Essa sintaxe é muito semelhante ao async/await, onde "esperamos" a execução de uma função e recebemos o seu resultado.
Note o asterisco necessário no fim de yield*.
Uma vantagem interessante do Result.gen() é que o TypeScript consegue inferir automaticamente todos os tipos de erro possíveis. Se validateItems pode retornar um InvalidItemError, calculateSubtotal pode retornar um CalculationError, e applyDiscount pode retornar um DiscountError, o TypeScript automaticamente deduz que a função retorna Result<number, InvalidItemError | CalculationError | DiscountError>, sem você precisar declarar explicitamente essa união de tipos.
Entendendo generators e yield* em detalhes
Generators permitem uma forma de inversão de controle, onde o fluxo de uma função é controlado de forma externa. Eles são geralmente utilizados para criar iteradores personalizados de forma preguiçosa ("lazy") ou para implementar máquinas de estado complexas.
O yield pausa a execução da função, envia um valor para fora e aguarda um comando para continuar. No momento que um generator é consumido, também é possível injetar valores de volta para dentro da função.
Para entender melhor como generators funcionam, considere três exemplos:
// Generator 1: um generator simples que rende (yield) valores
function* simpleGenerator() {
yield "primeiro";
yield "segundo";
yield "terceiro";
}
// Executando o generator chamando next() repetidas vezes
const gen1 = simpleGenerator();
console.log(gen1.next()); // { value: "primeiro", done: false }
console.log(gen1.next()); // { value: "segundo", done: false }
console.log(gen1.next()); // { value: "terceiro", done: false }
console.log(gen1.next()); // { value: undefined, done: true }
// Generator 2: um generator que delega para outro generator usando yield*
function* innerGenerator() {
yield "A";
yield "B";
}
function* outerGenerator() {
yield* innerGenerator(); // Delega para innerGenerator
yield "C";
}
// Executando o generator externo
const gen2 = outerGenerator();
console.log(gen2.next()); // { value: "A", done: false }
console.log(gen2.next()); // { value: "B", done: false }
console.log(gen2.next()); // { value: "C", done: false }
console.log(gen2.next()); // { value: undefined, done: true }
// Generator 3: um generator que RECEBE valores de volta
function* twoWayGenerator() {
// Quando você chama next(valor), esse valor é retornado pelo yield
const firstValue = yield "qual é seu nome?";
console.log(`Você respondeu: ${firstValue}`);
const secondValue = yield "qual é sua idade?";
console.log(`Você respondeu: ${secondValue}`);
return "fim!";
}
// Executando o generator com comunicação bidirecional
const gen3 = twoWayGenerator();
console.log(gen3.next()); // { value: "qual é seu nome?", done: false }
console.log(gen3.next("João")); // Envia "João" de volta para a variável firstValue
// Saída do console: "Você respondeu: João"
// { value: "qual é sua idade?", done: false }
console.log(gen3.next("25")); // Envia "25" de volta para a variável secondValue
// Saída do console: "Você respondeu: 25"
// { value: "fim!", done: true }
O yield* permite que generators deleguem toda a iteração para outros generators internos. Isso é exatamente o que acontece com Result.gen() e yield* - quando você faz yield* fetchOrder(orderId), você está delegando para aquela operação, e o Result.gen() extrai o resultado automaticamente.
Note também que você pode passar um argumento para .next(), e esse valor será retornado pela expressão yield anterior. Esse mecanismo de comunicação bidirecional é o que permite que Result.gen() funcione - quando você faz const order = yield* fetchOrder(orderId), o Result.gen() verifica o Result, e injeta o valor de sucesso de volta no generator (ou retorna o erro imediatamente).
// Usando generators com Result.gen()
async function processOrder(
orderId: string,
): AsyncResult<Order, ValidationError | DatabaseError | NotFoundError> {
return Result.gen(function* () {
// yield* "extrai" o valor do Result ou retorna o erro imediatamente
const order = yield* fetchOrder(orderId);
// Se chegou aqui, order é do tipo Order (não Result<Order, ...>)
const validatedOrder = yield* validateOrder(order);
// Continua extraindo valores de forma fluida
const savedOrder = yield* saveToDatabase(validatedOrder);
// Retorna o valor final
return savedOrder;
});
}
// Comparando com async/await (sintaxe muito similar)
async function fetchUserData(userId: string): Promise<UserData> {
// await "extrai" o valor da Promise
const user = await fetchUser(userId);
// Se chegou aqui, user é do tipo User (não Promise<User>)
const profile = await fetchProfile(user.id);
// Continua extraindo valores de forma fluida
const preferences = await fetchPreferences(user.id);
// Retorna o valor final
return { user, profile, preferences };
}
Esse padrão se aproxima do operador ? do Rust, que atua de forma semelhante, "extraindo" o valor
do Result ou retornando a função imediatamente com um erro:
// Operador ? em Rust funciona exatamente como yield* em generators
fn process_order(order_id: &str) -> Result<Order, OrderError> {
// ? extrai o valor ou retorna o erro imediatamente
let order = fetch_order(order_id)?;
// Se chegou aqui, order é do tipo Order (não Result<Order, ...>)
let validated_order = validate_order(order)?;
// Continua extraindo valores
let saved_order = save_to_database(validated_order)?;
// Retorna o valor final
Ok(saved_order)
}
Chegando ao um resultado final
Independente se você vai verificar cada Result individualmente, utilizar chains com .map() ou utilizar .gen(),
você geralmente vai chegar em um ponto onde o seu Result vai alcançar uma última camada, onde um valor deve ser gerado
ou um erro deve ser retornado.
O Result possui alguns métodos "getters" para quando você precisa gerar um valor, independendo do erro:
Os principais são getOrDefault(), que retorna um valor default no caso de um erro e getOrElse(), que executa
uma função de callback para adquirir o valor que deve ser retornado no caso de um erro.
// getOrDefault - retorna um valor padrão em caso de erro
async function getUserName(userId: string): Promise<string> {
const result = await fetchUser(userId);
// Se houver erro, retorna "Usuário Desconhecido"
return result.getOrDefault("Usuário Desconhecido");
}
// getOrElse - executa função para computar valor de fallback
async function getUserEmail(userId: string): Promise<string> {
const result = await fetchUser(userId);
// Se houver erro, computa um valor baseado no erro
return result.getOrElse((error) => {
console.error("Erro ao buscar usuário:", error);
return `user-${userId}@placeholder.com`;
});
}
// Exemplo prático: carregar configurações com fallback
async function loadConfig(): Promise<Config> {
return fetchRemoteConfig().getOrElse((error) => {
console.warn("Config remota indisponível, usando local:", error);
return loadLocalConfig();
});
}
Entretanto, em alguns casos, precisamos lidar com cada erro de forma diferente. Para isso, Results que são falhas
possuem um método chamado .match().
type PaymentError =
| { _tag: "InsufficientFunds"; required: number; available: number }
| { _tag: "InvalidCard"; reason: string }
| { _tag: "NetworkError"; message: string }
| { _tag: "SessionExpired" };
async function processPayment(amount: number): Promise<string> {
const result = await attemptPayment(amount);
if (!result.ok) {
// .match() permite encadear tratamentos para cada tipo de erro
return result
.match()
.when(
"InsufficientFunds",
(err) =>
`Saldo insuficiente. Necessário: R$${err.required}, Disponível: R$${err.available}`,
)
.when(
"InvalidCard",
(err) =>
`Cartão inválido: ${err.reason}. Por favor, verifique os dados.`,
)
.when(
"NetworkError",
(err) => `Erro de conexão: ${err.message}. Tente novamente.`,
)
.when("SessionExpired", () => `Sua sessão expirou. Faça login novamente.`)
.run();
}
return `Pagamento de R$${amount} processado com sucesso!`;
}
Graças ao Typescript, o .match() permite verificar que todos os erros são tratados, o que resultaria em erros de Typescript no caso de você fazer uma modificação que adiciona um novo tipo de erro, mas que não foi tratado.
Além disso, você pode utilizar o .else() para lidar com todos os outros erros da mesma forma.
type ApiError =
| { _tag: "NotFound"; resource: string }
| { _tag: "Unauthorized" }
| { _tag: "RateLimited"; retryAfter: number }
| { _tag: "ServerError"; message: string }
| { _tag: "NetworkError"; message: string };
async function fetchResource(id: string): Promise<string> {
const result = await getResource(id);
if (!result.ok) {
// Trata alguns erros especificamente, outros genericamente com .else()
return (
result
.match()
.when("NotFound", (err) => `Recurso "${err.resource}" não encontrado.`)
.when(
"Unauthorized",
() => `Você não tem permissão para acessar este recurso.`,
)
.when(
"RateLimited",
(err) =>
`Muitas requisições. Tente novamente em ${err.retryAfter} segundos.`,
)
// .else() captura todos os outros erros (ServerError, NetworkError, etc)
.else(
(err) => `Erro inesperado: ${err._tag}. Tente novamente mais tarde.`,
)
.run()
);
}
return result.value;
}
Utilizar o combinador .match() é ideal para lógicas de domínio onde cada erro exige uma reação específica (ex: erro de saldo -> mostrar modal; erro de sessão -> redirecionar login). Já o uso de .else() ou o tratamento genérico serve bem como um catch-all, garantindo que erros inesperados não quebrem a aplicação, mas tratando-os de forma unificada (ex: mostrar um "tente novamente mais tarde").
O .match() funciona somente em falhas, é necessário verificar o valor de .ok antes.
Conclusão
Mesmo que você não utilize especificamente a biblioteca typescript-result ou o padrão Result,
eu acredito que é importante você estar ciente dessas formas de tratar erros, e parar para pensar
em como você está lidando com erros na sua aplicação.
Se você tem interesse, eu recomendo ler a documentação do typescript-result, que possui
outros métodos e informações de como melhor utilizar a biblioteca.
Além disso, você pode explorar outras bibliotecas neverthrow e fp-ts. A biblioteca Effect vai além,
gerando um framework e ecossistemas em volta do conceito de Effect, uma função que recebe um contexto e retorna
um Result.