*"Lanzar o no lanzar, esa es la cuestión.* *Si es más noble para el código sufrir las flechas de los errores inesperados,* *o tomar las armas contra un mar de problemas y, oponiéndose a ellos, atraparlos."* ***Hamlet, Acto C#, Escena Try-Catch***

TL;DR (Resumen)

  • Las excepciones son costosas: Lanzar una excepción es 50-335 veces más lento que un simple if o un método Try*.
  • Cuándo usarlas: Para errores genuinamente excepcionales (red caída, archivo inexistente).
  • Cuándo NO usarlas: Para control de flujo normal, validación de entradas o en código crítico.
  • Alternativas: Usa TryParse, TryGetValue y similares cuando esperas errores frecuentes.
  • Mejor práctica: El simple hecho de tener bloques try/catch ralentiza tu código un 20%.

¿Qué es una excepción, realmente?

En C#, una excepción es un mecanismo para manejar errores de forma estructurada. Lanzar (throw) y capturar (try/catch) una excepción es más que un if con esteroides: implica crear un objeto, recopilar el stack trace, y alterar el flujo del programa bruscamente.

Es decir, no es gratis. Es como enviar a Ofelia al río con todas tus variables de error - dramático pero costoso.

La prueba del algodón: midiendo el coste

"La verdad se demuestra con hechos, no con palabras", como diría Polonio así que vamos a poner números al drama de las excepciones.

He preparado este simple escenario de batalla donde una excepción se enfrenta a una validación simple: Buscar una clave en nuestro diccionario.

[Benchmark]
public void ConIfSimple()
{
    // Escenario cotidiano: buscar un valor en un diccionario
    var dict = new Dictionary<string, int> { { "uno", 1 }, { "dos", 2 } };

    if (dict.ContainsKey("tres"))
        Console.WriteLine("Encontrado");
    else
        _ = 0; // No hacer nada            
}

[Benchmark]
public void ConExcepcionSinLanzar()
{
    // Mismo escenario pero con try-catch (sin llegar a lanzar)
    var dict = new Dictionary<string, int> { { "uno", 1 }, { "dos", 2 } };

    try
    {
        var valor = dict["tres"];
        Console.WriteLine("Encontrado");
    }
    catch (KeyNotFoundException) { _ = 0; }           
}

[Benchmark]
public void ConExcepcionLanzando()
{
    // Mismo escenario pero lanzando excepciones
    var dict = new Dictionary<string, int> { { "uno", 1 }, { "dos", 2 } };

    try
    {
        throw new KeyNotFoundException();
    }
    catch (KeyNotFoundException) { Console.WriteLine("Excepción lanzada"); }            
}

[Benchmark]
public void ConTryGetValue()
{
    // La alternativa óptima
    var dict = new Dictionary<string, int> { { "uno", 1 }, { "dos", 2 } };

    if (dict.TryGetValue("tres", out var valor))
        Console.WriteLine("Encontrado");
    else
        _ = 0; // No hacer nada            
}

Los resultados son tan dramáticos como la muerte de Ofelia:

| Method                   | Mean         | Error         |
|-------------------------:|-------------:|--------------:|
| ConIfSimple              | 0.2989 ms    |  2.4094 ms    |
| ConExcepcionSinLanzar    | 2.0568 ms    | 16.6938 ms    |
| ConExcepcionLanzando     | 3.0651 ms    | 24.9719 ms    |
| ConTryGetValue           | 0.2999 ms    |  2.4416 ms    |

¡Por todos los fantasmas de Elsinor! Veamos qué tenemos aquí:

  1. ConIfSimple: Nuestra línea base con un simple if. Eficiente y directo.
  2. ConExcepcionSinLanzar: El simple hecho de tener un try-catch ya tiene un gran coste (un 588% más), aunque nunca se lance la excepción.
  3. ConExcepcionLanzando: ¡925.4 % veces más lento! Lanzar excepciones es una tragedia para el rendimiento.
  4. ConTryGetValue: Sorprendentemente, el patrón TryGetValue es igual de rápido que el if manual, gracias a optimizaciones internas.

La diferencia es abismal: 925.4 % más lento usando excepciones para algo tan sencillo como buscar una clave en un diccionario.

¿Por qué es tan dramáticamente costoso lanzar excepciones?

  • Creación del objeto: Se instancia la excepción en memoria.
  • Captura del stack trace: La CLR recorre toda la pila de llamadas.
  • Búsqueda del handler: Se busca el primer catch compatible, desenrollando la pila.
  • Impacto en el JIT: Rompe optimizaciones como inlining.
  • Branch prediction fallida: Confunde al procesador que espera un flujo "normal".
  • Presión en el GC: Más objetos efímeros que recolectar después.

En resumen: lanzar excepciones es costoso como un desfile real en la corte de Dinamarca. Capturarlas aún más. Es como el soliloquio de Hamlet - extenso, dramático, y consume muchos recursos escénicos. Mientras que un simple "if" o un "TryParse" es como el directo "To be or not to be" - conciso y efectivo.

Esto no quiere decir que no debas usar nunca las excepciones o el try/catch. Solo que las uses para lo que realmente son: casos excepcionales y no controlables en tu código.

El pecado original: Antipatrones con excepciones

Algo huele a podrido en Dinamarca cuando vemos código como este:

public int ParseEdad(string valor)
{
    try 
    {
        return int.Parse(valor);
    }
    catch 
    {
        return 0; // Valor por defecto
    }
}

¡Horror! Este antipatrón es el equivalente a usar una espada para abrir un sobre. ¿Qué está mal aquí?

  1. Uso de excepciones como flujo de control normal: Las excepciones son para situaciones excepcionales, no para validaciones cotidianas.
  2. Catch genérico: Atrapa cualquier excepción, no solo FormatException.
  3. Esconde problemas reales: Si hay un error genuino, lo ignora silenciosamente.
  4. Rendimiento pésimo: Cada string inválido pagará el precio completo de generar y capturar una excepción.

¿Entonces nunca hay que usarlas?

¡No tan rápido, Horacio!

Como decía antes, las excepciones existen por algo. Usarlas bien mejora la claridad del código. Imagina una API:

User GetUserById(string id)

Si no encuentra el usuario, ¿qué hace?

  • ¿Devuelve null? (El fantasma de Polonio aparecerá en tus NullReferenceExceptions)
  • ¿Un bool de salida?
  • ¿Un TryGetUser como patrón?
  • ¿O lanza una excepción?

La respuesta es: depende del contrato del método.

Cuándo usar excepciones (con gusto)

  • Errores excepcionales: Un fichero que no existe, un índice fuera de rango, un error de red... No son parte del flujo normal del programa. Son excepciones, con todas las letras.

  • Precondiciones violadas: Si tu método especifica claramente que id no puede ser null, entonces ArgumentNullException es apropiado.

  • Validación externa: Si el usuario mete datos inválidos, lanzar una excepción es legítimo.

  • APIs públicas: Claridad > rendimiento. Es mejor lanzar ArgumentNullException que dejar que algo explote por null.

  • Para errores que no deberían ignorarse: Errores que requieren atención inmediata.

Cuándo evitarlas (por tu bien)

  • En bucles de alto rendimiento: Si tienes un método que se llama 100.000 veces por segundo, no deberías usar excepciones como control de flujo. Usa TryParse, TryGetValue, TryWhatever.

  • Para condiciones esperadas: ¿Un usuario no existe? Eso se espera. Devuelve null o usa un patrón como Result<T>. (En la segunda parte de este articulo hablaremos largo y tendido de él).

  • En código de bajo nivel o crítico: Networking, gráficos, procesamiento masivo... ahí cada milisegundo cuenta.

  • Para validar entrada de usuario: El usuario rara vez introduce datos perfectos. No conviertas cada validación en una excepción.

  • Para controlar flujo del programa: Las excepciones no son "GoTo's" sofisticados.

El Noble Arte del TryParse

¿Recuerdas nuestro ejemplo horrible de ParseEdad? He aquí su redención:

public int ParseEdad(string valor)
{
    if (int.TryParse(valor, out int resultado))
        return resultado;

    return 0; // Valor por defecto
}

POR SI LAS MOSCAS

Sé que eres una persona inteligente y seguramente ya lo sabrás, pero por si acaso, TryParse intenta parsear el valor que le pasas a la variable de salida; y devolverá un booleano indicando si la conversión tuvo exito o no.

Esta versión:

  • Es órdenes de magnitud más rápida
  • Expresa claramente la intención
  • No abusa del sistema de excepciones
  • Mantiene limpio el stack trace

Vamos a ver medir los tiempos de estos dos ejemplos en los que en un caso usamos el TryParse y en el otro capturamos la excepción cuando no se pueda parsear:

[Benchmark]
public void ParseConExcepcion()
{
    for (int i = 0; i < 1000; i++)
    {
        try { int.Parse("no-es-un-numero"); }
        catch { /* nada */ }
    }
}

[Benchmark]
public void ParseConTry()
{
    for (int i = 0; i < 1000; i++)
    {
        int.TryParse("no-es-un-numero", out _);
    }
}

Y los resultados del benchmark son:

|           Method   |        Mean  |     Error   |
|-------------------:|-------------:|------------:|
| ParseConExcepcion  | 3,612 ms     | 0,07196 ms  |
| ParseConTry        | 0,00732 ms   | 0,00014 ms  |

¡Casi 500 veces más lento con excepciones! ¡Es como comparar la velocidad de Hamlet decidiendo qué hacer con la del rayo!

La Familia Try: Tus Nuevos Mejores Amigos

El Framework .NET está lleno de métodos Try* que te ayudan a evitar excepciones:

  • int.TryParse, DateTime.TryParse, etc. - Para conversiones de tipos
  • Dictionary.TryGetValue - Para buscar en colecciones
  • File.Exists + File.Open - Para comprobar antes de abrir
  • int.TryFormat, DateTime.TryFormat - Para formateo de valores

Y muchas API modernas siguen este patrón. Aprende a reconocerlos y amarlos.

Alternativas modernas

Hoy en día hay soluciones intermedias más limpias que lanzar o morir:

public bool TryGetUser(string id, out User user)
{
    user = _repository.Find(id);
    return user != null;
}

O usando métodos que devuelven tuplas:

public (bool success, User? user) GetUser(string id)
{
    var user = _repository.Find(id);
    return (user != null, user);
}

También puedes implementar tus propios métodos Try siguiendo el patrón establecido:

public bool TryDividir(int numerador, int denominador, out int resultado)
{
    if (denominador == 0)
    {
        resultado = 0;
        return false;
    }

    resultado = numerador / denominador;
    return true;
}

Nos falta por nombrar un patrón muy interesante a la hora de gestionar alternativas a lanzar excepciones: el patrón Result. Pero esto lo veremos en la segunda parte del articulo ya que tiene chicha.

El Monólogo de la Destrucción: Empty Catch Blocks

"¡Oh, qué miserable práctica es el catch vacío! Menos malo es morir, que atrapar sin tratar."

El catch vacío es probablemente el pecado más grave en el manejo de excepciones:

try 
{
    // Algo peligroso
}
catch { } // ¡Horror y desolación!

Este patrón es el equivalente a barrer el polvo bajo la alfombra. Las excepciones ocurren por algo, e ignorarlas todas es invitar al desastre. Si un código así existe en tus aplicaciones es lo que suele llamar un Code smell de libro.

Como mínimo:

try 
{
    // Algo peligroso
}
catch (Exception ex)
{
    _logger.Error($"Operación fallida: {ex.Message}");
    // Quizás relanar algo apropiado
}

Recomendaciones finales - El Soliloquio de la Excepción

  • No tengas miedo de lanzar excepciones, pero hazlo con cabeza.
  • Nunca uses excepciones para control de flujo normal.
  • En código hot-path, evita lanzar, incluso evita try/catch si es posible.
  • Prefiere métodos tipo TryXxx cuando esperas errores comunes.
  • Cuando captures excepciones, hazlo de forma específica.
  • No dejes bloques catch vacíos. Al menos registra el error.
  • Usa finally para garantizar la limpieza de recursos.

Y si te encuentras escribiendo algo como:

try
{
    return Parse(input);
}
catch
{
    return defaultValue;
}

... probablemente haya un TryParse esperando a ser usado, y Yorick se estará revolviendo en su tumba.

Conclusión

Lanzar excepciones en C# no es el fin del mundo. Pero tampoco es gratis. Es una herramienta poderosa, útil para comunicar errores reales, no para validar si una cadena es un número.

Como nos enseñó Hamlet, la indecisión puede ser fatal. Pero en programación, decidir correctamente cuándo lanzar una excepción puede ser la diferencia entre una aplicación que brilla en escena o una que muere en el tercer acto.

¿Mi consejo final? Usa excepciones como los puñales envenenados de Shakespeare: reservados para los momentos verdaderamente dramáticos, no para cada pequeña escaramuza en tu código.


*Si te ha interesado este articulo no te pierdas el segundo acto donde intentaré explicar las diferencias entre throw;, throw ex;y throw new Exception(); El Arte de Relanzar Excepciones: Los Tres Destinos de un Error