No me lances una Exception si solo quieres validar un campo

Hey devs, nos ponemos ácidos: hay una mala costumbre muy extendida en el desarrollo backend: usar las excepciones como si fueran estructuras de control de flujo. Seguro que has visto (o escrito) código similar a esto en una capa de servicios o un comando en .NET:

public void RegistrarUsuario(string email)
{
    if (string.IsNullOrWhiteSpace(email))
    {
        throw new BusinessException("El email es obligatorio.");
    }
    
    // Continuar con el registro...
}

A primera vista parece limpio. Lanzas la excepción, un middleware global la captura en la capa de la API, la convierte en un 400 Bad Request y listo. El problema es que estás usando una herramienta diseñada para situaciones catastróficas o imprevistas (como perder la conexión a la base de datos o un disco lleno) para gestionar un escenario de negocio perfectamente predecible: que un usuario meta mal un dato.

El costo oculto del Stack Trace

En .NET, lanzar una excepción no es gratis. Cada vez que haces un throw, el CLR (Common Language Runtime) debe interrumpir el flujo normal de ejecución, capturar información de contexto y generar el Stack Trace para diagnosticar el error.

Si tu API recibe muchas peticiones concurrentes y una parte significativa falla por validaciones simples de formularios, el servidor terminará dedicando recursos innecesarios a crear y procesar excepciones. En escenarios de alta carga, esto puede incrementar la latencia y reducir el rendimiento general de la aplicación.

En casos extremos, cuando las excepciones se producen de forma masiva y sostenida, este costo adicional puede contribuir a problemas de rendimiento más amplios, incluyendo aumentos de latencia e incluso situaciones de Thread Pool Starvation.

Las excepciones deben ser, por definición, excepcionales. Obvio, pero no tan obvio jaja.

El patrón Result: Control de flujo explícito

La alternativa elegante y eficiente es usar el patrón Result. En lugar de lanzar una bomba atómica cuando algo falla, el método devuelve un objeto que indica explícitamente si la operación fue exitosa o no, junto con el valor o el error correspondiente.

Una implementación simple en .NET se ve así:

public class Result<T>
{
    public bool IsSuccess { get; }
    public T? Value { get; }
    public string? Error { get; }

    private Result(bool success, T? value, string? error)
    {
        IsSuccess = success;
        Value = value;
        Error = error;
    }

    public static Result<T> Success(T value) => new(true, value, null);
    public static Result<T> Failure(string error) => new(false, default, error);
}

Ahora, tu lógica de negocio se vuelve predecible, tipada y ridículamente rápida:

public Result<Usuario> RegistrarUsuario(string email)
{
    if (string.IsNullOrWhiteSpace(email))
        return Result<Usuario>.Failure("El email es obligatorio.");

    if (_repository.ExisteEmail(email))
        return Result<Usuario>.Failure("El email ya está registrado.");

    var nuevoUsuario = new Usuario(email);
    return Result<Usuario>.Success(nuevoUsuario);
}

Código más honesto

Cuando miras la firma de un método que devuelve void o Usuario, el compilador te está mintiendo. No sabes qué trampas (exceptions) hay ocultas dentro de la implementación.

Al devolver un Result<Usuario>, tu código es lo que yo llamaría honesto. Le estás diciendo al programador que consuma tu método: "Ojo, esto puede fallar por razones de negocio, y estás obligado a manejar el resultado" sin penalizar el rendimiento del servidor.

Si te fascinó el artículo, comparte, vamos crack!

Créditos de imagen de portada: Foto de Wiseman Mabasa en Unsplash

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *