miércoles, 23 de agosto de 2017

Cuestiónate todo: Throw vs Throw ex

Hace unos días leía una entrada sobre excepciones en Campus MVP: GAMBADAS: Los tres principales pecados al gestionar errores y excepciones. Allí nos daban consejos para trabajar con excepciones, y entre ellos nos recordaban como debemos relanzar una excepción, y cual es la diferencia entre relanzarla haciendo throw o haciendo throw ex. Me refiero a esto:

image

En el artículo se indicaba que cuando relanzamos una excepción haciendo throw ex estamos “reseteando” el call stack, por lo que un elemento superior que capture la excepción no va a poder saber donde se generó originalmente,y obtendrá como origen la línea en la que hacemos el throw ex.

image

Según lo leí la cabeza me jugo una mala pasada y me dije: “eso esta mal, hacer throw o throw ex es lo mismo”. Lo malo es que rápidamente me fui a Twitter a pregonarlo, donde me corrigieron a la primera de cambio: no no es lo mismo. Si nos vamos a la documentación oficial de C# (https://docs.microsoft.com/es-es/dotnet/csharp/language-reference/keywords/throw) vemos esto:
image

Parece estar meridianamente claro… ¿pero es así en todos los casos?  ¿De donde había sacado yo las idea de que era lo mismo una cosa que la otra?

El ejemplo

Vamos a comprobarlo, para ello he creado un código de ejemplo, es sencillo. Tenemos un método  que lanza una excepción, ya que hace una división por cero
public static int FaultyCode(int number)
{
    int divisor = 0;
    return number / divisor;
}
Luego tenemos un segundo método (CatchAndThrowEx) llama al anterior, recubriendo la llamada en un bloque try/catch. Una vez capturada la excepción la relanza con throw ex.
public static void CatchAndThrowEx()
{
    try
    {
        var result = FaultyCode(5);
    }
    catch (Exception ex)
    {
        throw ex;
    }
}
También un tercer método(CatchAndThrow) similar al anterior y que también llama al código que falla. La llamada es recubierta en un bloque try/catch. Una vez capturada la excepción la relanza con throw.
public static void CatchAndThrow()
{
    try
    {
        var result = FaultyCode(5);
    }
    catch(Exception ex)
    {
        throw;
    }
}
Finalmente, desde el main de una aplicación de consola, invocamos a los dos métodos que relanzan la excepción, y la capturamos, trazando su call stack, para que veamos si este ha sido “reseteado” o no.

public static void Main(string[] args)
{
    try
    {
        Console.WriteLine("==============================================");
        Console.WriteLine("CatchAndThrow:");
        CatchAndThrow();
    }
    catch (Exception ex)
    {
        ex.ToConsole();
    }

    try
    {

        Console.WriteLine("==============================================");
        Console.WriteLine("CatchAndThrowEx:");
        CatchAndThrowEx();
    }
    catch (Exception ex)
    {
        ex.ToConsole();
    }
}
Si ejecutamos lo anterior, obtenemos lo esperado: cuando se relanza con throw ex se pierde el call stack y no somos capaces de ver el método donde se originó la excepción… malo
Core 2

Seguimos cuestionando

Antes de abandonar me dije, lo he ejecutado compilando en  debug… ¿habrá cambio en release?
Si ejecuto en release (dotnet run -c Release) las cosas cambian!  Como podéis ver a continuación throw y throw ex se comportan exactamente igual.
image

¡Esto no estaba en el guión!
Visto lo visto me dije, será cosa de .Net Core 2? Vamos a comprobar en otras configuraciones. El resultado está en la siguiente tabla, y siempre es el mismo: cuando se ha compilado en release throw y throw ex se comportan igual:

PlataformaCompilaciónResultado
.Net Core 1.1 Debug Throw mantiene el call stack. Throw ex no lo hace
.Net Core 1.1 Release Throw y Throw ex tienen el mismo comportamiento
.Net Core 2 Debug Throw mantiene el call stack. Throw ex no lo hace
.Net Core 2 Release Throw y Throw ex tienen el mismo comportamiento
.Net Framework 4.6.2  Debug Throw mantiene el call stack. Throw ex no lo hace
.Net Framework 4.6.2  Release Throw y Throw ex tienen el mismo comportamiento 



¿Donde está el truco?

Suele haber un motivo, siempre lo hay, que explique comportamientos extraños, comportamiento que contradicen la documentación oficial como es en este caso.

Yo ni lo había imaginado, pero Juanma (https://twitter.com/gulnor) rápidamente intuyó que pasaba: El método FaultyCode era demasiado sencillo, tanto que al compilar en Release se convierte en un inline, por lo que pasa a ser código del método que le invoca.

Si complicamos FaultyCode, el compilador no lo convertirá en un inline, y finalmente  veremos que el comportamiento vuelve a ser el esperado, y el documentado:

  • Throw mantiene el call stack
  • Throw ex lo resetea


Saludos, espero que os haya resultado interesante

No hay comentarios:

Publicar un comentario