Fabrizio Feitosa

Fabrizio Feitosa

Exploro o universo do desenvolvimento com artigos, tutoriais e reflexões sobre tecnologia — compartilhando aprendizados reais e dicas práticas do dia a dia.


Voltar

Simplifique o tratamento de erros em TypeScript com este wrapper inteligente

Descubra como criar um wrapper TypeScript que elimina a repetição de try/catch e torna o tratamento de erros mais elegante e produtivo


Se você já desenvolveu em TypeScript, provavelmente já se cansou de escrever blocos try/catch repetitivos em todo o seu código. Essa repetição não apenas torna o código verboso, mas também dificulta a manutenção e a legibilidade.

Neste artigo, vou mostrar como criar um wrapper TypeScript inteligente que simplifica drasticamente o tratamento de erros, tornando seu código mais limpo e produtivo.

O problema: Try/Catch repetitivo

Imagine que você tem várias funções que podem lançar erros:

async function buscarUsuario(id: string) {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      throw new Error("Usuário não encontrado");
    }
    return await response.json();
  } catch (error) {
    console.error("Erro ao buscar usuário:", error);
    throw error;
  }
}
 
async function criarPost(dados: PostData) {
  try {
    const response = await fetch("/api/posts", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(dados),
    });
    if (!response.ok) {
      throw new Error("Falha ao criar post");
    }
    return await response.json();
  } catch (error) {
    console.error("Erro ao criar post:", error);
    throw error;
  }
}

Repetitivo, não é? Vamos criar uma solução melhor.

A solução: Wrapper TypeScript simples

Vamos começar com uma versão mais simples e direta:

export const safeAwait = async <T>(
  promise: Promise<T>
): Promise<[T | null, Error | null]> => {
  try {
    const data = await promise;
    return [data, null];
  } catch (error) {
    return [null, error instanceof Error ? error : new Error(String(error))];
  }
};

Como usar o safeAwait básico

Esta versão retorna uma tupla [dados, erro] que é muito fácil de usar:

import { safeAwait } from "./safeAwait";
 
const buscarDados = async () => {
  const [data, error] = await safeAwait(fetch("https://api.example.com/data"));
 
  if (error) {
    console.error("Erro ao buscar dados:", error);
    return;
  }
 
  return data;
};

Exemplo prático com interface

interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}
 
const buscarPosts = async (): Promise<Post[]> => {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts");
  return response.json();
};
 
const buscarDados = async () => {
  const [data, error] = await safeAwait(buscarPosts());
  if (error) {
    console.error("Erro ao buscar posts:", error);
    return;
  }
 
  return data;
};

A solução: Wrapper TypeScript avançado

Para casos mais complexos, vamos criar um wrapper mais robusto que encapsula a lógica de tratamento de erros:

type Result<T, E = Error> =
  | {
      success: true;
      data: T;
    }
  | {
      success: false;
      error: E;
    };
 
async function tryCatch<T>(
  fn: () => Promise<T> | T,
  errorHandler?: (error: unknown) => void
): Promise<Result<T>> {
  try {
    const result = await fn();
    return {
      success: true,
      data: result,
    };
  } catch (error) {
    if (errorHandler) {
      errorHandler(error);
    }
    return {
      success: false,
      error: error as Error,
    };
  }
}

Como usar o wrapper avançado

Com o wrapper avançado, suas funções ficam muito mais limpas:

async function buscarUsuario(id: string) {
  return tryCatch(
    async () => {
      const response = await fetch(`/api/users/${id}`);
      if (!response.ok) {
        throw new Error("Usuário não encontrado");
      }
      return await response.json();
    },
    (error) => {
      console.error("Erro ao buscar usuário:", error);
    }
  );
}
 
async function criarPost(dados: PostData) {
  return tryCatch(
    async () => {
      const response = await fetch("/api/posts", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(dados),
      });
      if (!response.ok) {
        throw new Error("Falha ao criar post");
      }
      return await response.json();
    },
    (error) => {
      console.error("Erro ao criar post:", error);
    }
  );
}

Usando o resultado

O wrapper avançado retorna um objeto Result que você pode usar de forma type-safe:

async function exemploDeUso() {
  const resultado = await buscarUsuario("123");
 
  if (resultado.success) {
    // TypeScript sabe que resultado.data existe aqui
    console.log("Usuário encontrado:", resultado.data);
  } else {
    // TypeScript sabe que resultado.error existe aqui
    console.log("Erro:", resultado.error.message);
  }
}

Wrapper com configurações avançadas

Para casos mais complexos, você pode criar uma versão mais robusta:

interface TryCatchOptions {
  onError?: (error: unknown) => void;
  onSuccess?: <T>(data: T) => void;
  retryCount?: number;
  retryDelay?: number;
}
 
async function tryCatchAdvanced<T>(
  fn: () => Promise<T> | T,
  options: TryCatchOptions = {}
): Promise<Result<T>> {
  const { onError, onSuccess, retryCount = 0, retryDelay = 1000 } = options;
 
  for (let attempt = 0; attempt <= retryCount; attempt++) {
    try {
      const result = await fn();
 
      if (onSuccess) {
        onSuccess(result);
      }
 
      return {
        success: true,
        data: result,
      };
    } catch (error) {
      if (onError) {
        onError(error);
      }
 
      if (attempt === retryCount) {
        return {
          success: false,
          error: error as Error,
        };
      }
 
      // Aguarda antes da próxima tentativa
      await new Promise((resolve) => setTimeout(resolve, retryDelay));
    }
  }
 
  // Nunca deve chegar aqui, mas TypeScript exige
  throw new Error("Unexpected error");
}

Exemplo com retry automático

async function buscarDadosComRetry() {
  return tryCatchAdvanced(
    async () => {
      const response = await fetch("/api/dados");
      if (!response.ok) {
        throw new Error("Falha na requisição");
      }
      return await response.json();
    },
    {
      retryCount: 3,
      retryDelay: 2000,
      onError: (error) => console.error("Tentativa falhou:", error),
      onSuccess: (data) => console.log("Dados obtidos com sucesso:", data),
    }
  );
}

Wrapper para funções síncronas

Para funções que não são assíncronas:

function tryCatchSync<T>(
  fn: () => T,
  errorHandler?: (error: unknown) => void
): Result<T> {
  try {
    const result = fn();
    return {
      success: true,
      data: result,
    };
  } catch (error) {
    if (errorHandler) {
      errorHandler(error);
    }
    return {
      success: false,
      error: error as Error,
    };
  }
}

Comparando as duas abordagens

safeAwait (Simples)

  • Retorna: Tupla [T | null, Error | null]
  • Uso: const [data, error] = await safeAwait(promise)
  • Ideal para: Casos simples e diretos
  • Vantagem: Sintaxe muito limpa e familiar

tryCatch (Avançado)

  • Retorna: Objeto Result<T> com propriedades success, data e error
  • Uso: const result = await tryCatch(fn)
  • Ideal para: Casos complexos com handlers personalizados
  • Vantagem: Type safety mais robusto e flexibilidade

Qual escolher?

  • Use safeAwait quando quiser uma solução rápida e simples
  • Use tryCatch quando precisar de mais controle e funcionalidades avançadas

Benefícios do wrapper

  1. Código mais limpo: Elimina a repetição de try/catch
  2. Type safety: TypeScript garante que você trate os casos de sucesso e erro
  3. Flexibilidade: Fácil de customizar com handlers de erro
  4. Reutilização: Uma vez criado, pode ser usado em todo o projeto
  5. Manutenibilidade: Mudanças na lógica de tratamento de erros são centralizadas

Considerações finais

Este wrapper TypeScript é uma ferramenta poderosa que pode transformar a forma como você lida com erros em seus projetos. Ele não apenas reduz a repetição de código, mas também torna o tratamento de erros mais consistente e type-safe.

Para implementar em seu projeto, basta copiar o código do wrapper e começar a usar. Você verá imediatamente a diferença na legibilidade e manutenibilidade do seu código.

Lembre-se: o objetivo não é esconder erros, mas sim tratá-los de forma mais elegante e produtiva. O wrapper ainda permite que você capture e trate erros adequadamente, mas de uma forma muito mais limpa.

Experimente em seu próximo projeto e veja como isso pode melhorar sua experiência de desenvolvimento!


Créditos: Este artigo foi baseado no post original "Stop Repeating Try/Catch: This TypeScript Wrapper Makes Error Handling Easy" por Angel Serrano, traduzido e adaptado para português.