← Back to list

Uso de Mônadas na Programação: Reduzindo Exceptions com Either em C#

Descubra como a adoção de princípios funcionais e o uso de mônadas, como Either, podem reduzir drasticamente o uso de exceções em C#.

Rafael Luis da Costa Coelho · 2025-02-05 02:39 · 1 claps · 6.9 min read
#either #mônada #csharp #exception-handling #programação-funcional
Open on Medium ↗

Uso de Mônadas na Programação: Reduzindo Exceptions com Either em C

Figura 01 - Está chovendo exceptions por aí?

Figura 01 - Está chovendo exceptions por aí?

Vamos explorar um problema comum no desenvolvimento de software: as Exceptions. Para isso, é essencial introduzirmos alguns conceitos fundamentais que nos ajudarão a compreender sua natureza, suas causas e as melhores práticas para lidar com elas de maneira eficaz.

A programação funcional é um paradigma que trata a computação como a avaliação de funções matemáticas e evita estados mutáveis e efeitos colaterais. Diferente da programação imperativa, onde o estado do programa é modificado por meio de variáveis, a programação funcional se baseia em imutabilidade e funções puras. Esse paradigma tem ganhado popularidade devido à sua previsibilidade e facilidade de composição, tornando o código mais robusto e seguro.

A programação funcional tem introduzido diversos conceitos poderosos para lidar com a complexidade do código, um deles sendo as mônadas. Em linguagens como Haskell e Scala, o uso de mônadas é amplamente difundido para tratar erros e computações encadeadas de forma segura.

Origem do Conceito de Mônadas

O termo “mônada” vem da teoria das categorias, um ramo da matemática que estuda estruturas e suas transformações. O conceito foi introduzido na programação funcional por Eugenio Moggi na década de 1990 para modelar efeitos computacionais, como manipulação de estado, entrada e saída (I/O) e tratamento de erros, de maneira pura.

Em Haskell, mônadas são amplamente utilizadas para estruturar programas funcionais sem efeitos colaterais explícitos. Outras linguagens funcionais, como Scala e F#, também adotaram o conceito. Em linguagens imperativas como C#, conceitos similares podem ser implementados para melhorar a previsibilidade do código e facilitar sua manutenção.

No C#, embora não seja uma linguagem funcional pura, podemos nos beneficiar de padrões similares utilizando estruturas como Either para reduzir o uso excessivo de exceções.

O Problema com Exceptions

Exceções são um mecanismo tradicional em C# para lidar com erros, mas possuem desvantagens significativas:

  • Custo de desempenho: O lançamento e a captura de exceções são operações computacionalmente dispendiosas. Internamente, o runtime do .NET precisa capturar a pilha de chamadas (stack trace), alocar objetos de exceção e percorrer blocos catch, o que pode impactar o desempenho da aplicação, especialmente em cenários de alto throughput.
  • Dificuldade na composição: O uso de exceções não se encaixa bem em um modelo de composição funcional. Como elas interrompem abruptamente o fluxo do programa e precisam ser tratadas externamente, encadear operações que podem falhar torna-se difícil sem recorrer a blocos try-catch repetitivos.
  • Ocultamento da lógica de erro: Como exceções desviam o fluxo normal do programa, elas dificultam a leitura e a compreensão do código. Um método que lança exceções não deixa explícito quais tipos de falhas podem ocorrer, forçando o desenvolvedor a consultar documentação ou inspecionar a implementação para prever os cenários de erro.
  • Impacto na testabilidade: O tratamento de exceções pode tornar o código menos previsível e mais difícil de testar. Para capturar e validar um erro em testes automatizados, é necessário recorrer a verificações explícitas de exceção, enquanto uma abordagem baseada em valores retornados permite testar fluxos alternativos com maior facilidade.
  • Problemas com concorrência: Em sistemas assíncronos e concorrentes, lidar com exceções pode ser ainda mais desafiador. Uma exceção lançada em uma Task pode ser silenciosamente ignorada se não for capturada corretamente, levando a falhas difíceis de diagnosticar.

Introduzindo Either

Either é uma estrutura de dados que pode conter dois tipos de valores: um valor de erro (geralmente chamado de “Left”) ou um valor válido (“Right”). A notação “Left” para erro e “Right” para sucesso segue uma convenção da comunidade funcional, onde “Right” também significa “correto”.

Uma implementação simples de Either em C# poderia ser:

public class Either<TLeft, TRight>
{
    private readonly TLeft _left;
    private readonly TRight _right;
    private readonly bool _isRight;

    private Either(TLeft left)
    {
        _left = left;
        _right = default!;
        _isRight = false;
    }

    private Either(TRight right)
    {
        _right = right;
        _left = default!;
        _isRight = true;
    }

    public static Either<TLeft, TRight> Left(TLeft left) => new Either<TLeft, TRight>(left);
    public static Either<TLeft, TRight> Right(TRight right) => new Either<TLeft, TRight>(right);

    /// <summary>
    /// Permite mapear a estrutura para um valor de saída,
    /// executando a função adequada conforme seja Left ou Right.
    /// </summary>
    public TResult Match<TResult>(Func<TLeft, TResult> leftFunc, Func<TRight, TResult> rightFunc)
    {
        return _isRight ? rightFunc(_right) : leftFunc(_left);
    }

    /// <summary>
    /// Encadeia operações que podem falhar, permitindo composição funcional.
    /// Se for Left, apenas propaga o erro; se for Right, aplica 'binder'.
    /// </summary>
    public Either<TLeft, TNewRight> Bind<TNewRight>(
        Func<TRight, Either<TLeft, TNewRight>> binder)
    {
        return _isRight ? binder(_right) : Either<TLeft, TNewRight>.Left(_left);
    }
}

Exemplo de Uso: Validação de Entrada sem Exceptions

Vamos supor que temos um sistema de autenticação onde validamos um usuário baseado em suas credenciais. Tradicionalmente, poderíamos lançar exceções para indicar falhas na autenticação (exemplo hipotético):

// Exemplo tradicional com exceções
public User AuthenticateOrThrow(string username, string password)
{
    if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
    {
        throw new ArgumentException("Credenciais inválidas");
    }

    var user = Database.GetUser(username);
    if (user == null || !user.VerifyPassword(password))
    {
        throw new UnauthorizedAccessException("Usuário ou senha incorretos");
    }

    return user;
}

Com Either, podemos reformular esse código para retornar um resultado mais previsível:

// Exemplo reformulado com Either
public Either<string, User> Authenticate(string username, string password)
{
    if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
    {
        return Either<string, User>.Left("Credenciais inválidas");
    }

    var user = Database.GetUser(username);
    if (user == null || !user.VerifyPassword(password))
    {
        return Either<string, User>.Left("Usuário ou senha incorretos");
    }

    return Either<string, User>.Right(user);
}

Agora, ao consumir essa função, tratamos o erro explicitamente:

var result = Authenticate("rafael", "senha123");
result.Match(
    error => Console.WriteLine($"Erro: {error}"),
    user  => Console.WriteLine($"Usuário autenticado: {user.Name}")
);

Dessa forma, não precisamos de try-catch; o próprio tipo Either nos obriga a lidar tanto com o caso de sucesso quanto com o caso de erro.

Automatizando o Retorno de Mônadas via REST com Filters

No desenvolvimento de APIs REST em C#, podemos utilizar filters para transformar os retornos de Either em respostas HTTP adequadas de forma automática. Isso evita a necessidade de verificações manuais em cada endpoint.

Podemos, por exemplo, criar um filtro chamado EitherResultFilter que intercepta o objeto retornado pela controller antes de gerar a resposta HTTP. Esse filtro poderia mapear as instâncias de Either para os códigos de status correspondentes (status code), tornando o tratamento de erros mais padronizado. Um exemplo simplificado:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

public class EitherResultFilter : IResultFilter
{
    public void OnResultExecuting(ResultExecutingContext context)
    {
        if (context.Result is ObjectResult objectResult && 
            objectResult.Value != null)
        {
            // Verifica se o valor é algum Either<,> dinamicamente
            var valueType = objectResult.Value.GetType();
            if (valueType.IsGenericType && 
                valueType.GetGenericTypeDefinition() == typeof(Either<,>))
            {
                // Extração dinâmica do Left e Right, caso deseje manipular especificamente
                // Para simplificar, faremos um Match básico (reflexão seria necessária
                // se quisermos extrair TLeft e TRight diretamente).

                // Suponhamos que tenhamos um método auxiliar para lidar com reflection:
                context.Result = ProcessEither(objectResult.Value);
            }
        }
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
        // Não fazemos nada aqui por enquanto
    }

    private IActionResult ProcessEither(object eitherValue)
    {
        // Exemplo extremamente simplificado:
        // * Supondo que TLeft seja string (mensagem de erro)
        // * TRight seja algum objeto de sucesso
        // Você pode tornar isso mais sofisticado por reflection ou pattern matching.

        dynamic dynamicEither = eitherValue;
        return dynamicEither.Match<IActionResult>(
            left =>
            {
                // Defina a lógica para diferentes tipos de erro
                // Exemplo: se for "NotFound", retorna 404, etc.
                if (left.Contains("não encontrado", StringComparison.OrdinalIgnoreCase))
                {
                    return new NotFoundObjectResult(left);
                }
                // Caso genérico de erro -> BadRequest
                return new BadRequestObjectResult(left);
            },
            right =>
            {
                // Caso seja nulo, pode retornar NoContent (204)
                if (right == null)
                    return new NoContentResult();
                // Caso seja autenticado, retorna 200 (OK)
                return new OkObjectResult(right);
            }
        );
    }
}

Em seguida, basta registrar o filtro globalmente em Startup.cs ou Program.cs (dependendo da versão do .NET):

services.AddControllers(options => 
{
    options.Filters.Add<EitherResultFilter>();
});

Com esse filtro, o retorno de uma API que usa Either pode ser automaticamente traduzido para:

  • BadRequest (400) para erros genéricos.
  • NotFound (404) quando a entidade solicitada não existe (ou mensagem de erro que implique “não encontrado”).
  • Unauthorized (401) para problemas de autenticação (você pode adaptar a lógica no filtro).
  • Forbidden (403) para tentativas de acesso não permitido.
  • NoContent (204) quando o retorno válido é null.
  • Ok (200) para respostas bem-sucedidas.

Isso melhora a manutenibilidade da API e reduz a repetição de código.

Benefícios do Uso de Either

O uso de mônadas, como Either, oferece diversas vantagens para o desenvolvimento de software em C#:

  • Fluxo de controle previsível: Em vez de usar try-catch, o erro é tratado como um valor explícito. Isso melhora a previsibilidade do fluxo de execução, eliminando a necessidade de capturar e propagar exceções que podem ser inesperadas.
  • Maior clareza no código: O leitor do código sabe que a função pode falhar e é forçado a tratar esse caso de maneira explícita. Isso melhora a legibilidade e reduz ambiguidades no tratamento de erros, tornando mais fácil a manutenção do código.
  • Melhor manutenibilidade: A separação clara entre lógica de sucesso e erro reduz a necessidade de blocos try-catch, tornando o código mais legível e fácil de manter.
  • Melhor desempenho: Evitamos o custo adicional de exceções, que envolvem captura de stack trace e manipulação pelo runtime. Em sistemas de grande escala, onde milhões de operações são executadas simultaneamente, reduzir o uso de exceções pode diminuir significativamente a sobrecarga de CPU e memória.
  • Facilidade de composição: Podemos encadear operações de forma funcional sem quebrar o fluxo com exceções. Isso permite que diferentes operações que podem falhar sejam combinadas de maneira fluida e previsível, resultando em código mais modular e reutilizável.
  • Melhor comportamento em sistemas distribuídos: Em aplicações baseadas em microsserviços, a propagação de exceções pode ser problemática e difícil de rastrear. Either permite que os serviços comuniquem falhas de maneira mais estruturada e serializável, facilitando a depuração e observabilidade do sistema.
  • Facilidade de integração com APIs REST: O uso de Either permite padronizar respostas HTTP sem a necessidade de verificações manuais, tornando a API mais consistente.

Conclusão

O uso de mônadas, como Either, traz maior previsibilidade e clareza para a manipulação de erros em C#. Além disso, ao automatizar o tratamento de respostas REST, podemos garantir que as APIs retornem respostas apropriadas sem a necessidade de lógica repetitiva. A aplicação de conceitos funcionais no C# permite construir software mais seguro, modular e eficiente.

Se você busca uma abordagem mais funcional para lidar com erros e composição de operações em C#, considere integrar Either e outros padrões funcionais em seus projetos!

Nos próximos artigos, vamos demonstrar na prática, utilizando OpenTelemetry, as métricas resultantes desta pesquisa. Fique ligado e até breve!

Referências científicas e fundamentos

  • Moggi, E. (1991). Notions of Computation and Monads. Information and Computation, 93(1).
  • Wadler, P. (1995). Monads for functional programming. In Advanced Functional Programming (pp. 24–52). Springer.
  • Meijer, E., Fokkinga, M., & Paterson, R. (1991). Functional programming with bananas, lenses, envelopes and barbed wire. In Conference on Functional Programming Languages and Computer Architecture.

메타데이터
post_id
8c3d68c413f2
slug
uso-de-mônadas-na-programação-reduzindo-exceptions-com-either-em-c-8c3d68c413f2
url
https://medium.com/@rafaelluisdacostacoelho/uso-de-m%C3%B4nadas-na-programa%C3%A7%C3%A3o-reduzindo-exceptions-com-either-em-c-8c3d68c413f2
canonical_url
https://medium.com/@rafaelluisdacostacoelho/uso-de-m%C3%B4nadas-na-programa%C3%A7%C3%A3o-reduzindo-exceptions-com-either-em-c-8c3d68c413f2
author_url
https://medium.com/@rafaelluisdacostacoelho
status
ok
fetched_at
2026-06-23 17:05:31