10 dic 2015

Excepciones y más Excepciones (o el manejo de Errores)

Como sabemos las excepciones son el mecanismo por medio del cual el .Net Framework maneja los errores, éstas se “lanzan” cuando hay algo que produce un error, una excepción, un comportamiento indeseado y no manejado en nuestro código. Sabemos también que debemos darle tratamiento, no podemos ignorarlas, ya que harán que una aplicación deje de funcionar, se cuelgue, se cierre, o muestre horribles mensajes o páginas con las excepciones “crudas” al usuario final.

El esquema que solemos utilizar para manejar las excepciones son los bloques try{} catch{} finally{}, donde lo que deseamos ejecutar va dentro del bloque try, el tratamiento del error, si se produjese, va en el bloque cacth y lo que ejecutaremos siempre al final independientemente si se produjeron o no errores (liberar recursos por ejemplo) irá en el bloque finally

Exception Bubbling


Supongamos que tenemos varios (muchos) métodos o funciones que se llaman entre sí, de manera que el método 1 llama al método 2 que a su vez llama al método 3  y así consecutivamente hasta “n” métodos. ¿Qué sucede si en el método “n” se produce un error? Entra a jugar el mecanismo que se conoce como “exception bubbling” el cual consiste en que el error es “lanzando hacia arriba”, hacia el método que llamó al que produce el error, se añade información a la pila de seguimiento (StackTrace) y el error es nuevamente “lanzado hacia arriba” reproduciendo el mismo comportamiento hasta llegar al primer método de la cadena o hasta alguno en el que se dé un tratamiento apropiado. El siguiente código ilustra este comportamiento:

using System;

namespace BubblingExceptions
{
    internal static class Program
    {
        private static void Main(string[] args)
        {
            errorbubbling();
        }

        private static void errorbubbling()
        {
            try
            {
                Console.WriteLine("Inicia ejecución >> Llamando a Método 1");
                Metodo1();
                Console.WriteLine("Ejecución exitosa de Método 1");
            }
            catch (Exception ex)
            {
                Console.WriteLine("\nHa ocurrido una excepción:\n\n" + ex);
            }
            finally
            {
                Console.WriteLine("\nEjecutado bloque finally.");
                Console.ReadKey();
            }
        }

        private static void Metodo1()
        {
            Console.WriteLine("Método 1 >> Llamando al Método 2");
            Metodo2();
            Console.WriteLine("Ejecución exitosa de Método 2");
        }

        private static void Metodo2()
        {
            Console.WriteLine("Método 2 >> Llamando al Método 3");
            Metodo3();
            Console.WriteLine("Ejecución exitosa de Método 3");
        }

        private static void Metodo3()
        {
            Console.WriteLine("Método 3 >> Llamando al Método 4");
            Metodo4();
            Console.WriteLine("Ejecución exitosa de Método 4");
        }

        private static void Metodo4()
        {
            Console.WriteLine("Método 4 >> Llamando al Método 5");
            Metodo5();
            Console.WriteLine("Ejecución exitosa de Método 5");
        }

        private static void Metodo5()
        {
            Console.WriteLine("Método 5");
            throw new Exception("Exception de prueba de bubbling");
        }
    }
}

Como puede verse la excepción es lanzada desde el método más profundo, el número 5, hasta el que inicia la ejecución,  errorbubbling, mientras que en el camino recolecta información que es añadida a la pila de seguimiento. 

Por esto es que resulta imprescindible aprender a leer el StackTrace, el cual nos brindará información valiosa cuando estamos depurando nuestro código. Por ejemplo, en el resultado anterior, podemos saber que el error originalmente se produjo en la línea 62 del metodo5, y que este se llamó desde la línea 55 del método4, recorremos los llamados hasta encontrarnos que el error inicia en la línea 17 del método que comenzó la ejecución.

Aquí es importante reafirmar una regla de oro en el tratamiento de las excepciones: solo se captura una excepción (usando el catch)  cuando vamos a hacer algo productivo con ella, que en la mayoría de los casos debería ser tratarla adecuadamente impidiendo el bubbling (detener la propagación de la excepción) y, probable e idealmente, registrar el error en una bitácora (log).  Esto se ilustra de la siguiente manera:

private static void errorbubbling()
{
    try
    {
        Console.WriteLine("Inicia ejecución >> Llamando a Método 1");
        Metodo1();
        Console.WriteLine("Ejecución exitosa de Método 1");
    }
    catch (Exception ex)
    {
        Console.WriteLine("\nHa ocurrido una excepción:\n\n" + ex);
    }
    finally
    {
        Console.WriteLine("\nEjecutado bloque finally.");
        Console.ReadKey();
    }
}

private static void Metodo1()
{
    Console.WriteLine("Método 1 >> Llamando al Método 2");
    Metodo2();
    Console.WriteLine("Ejecución exitosa de Método 2");
}

private static void Metodo2()
{
    Console.WriteLine("Método 2 >> Llamando al Método 3");
    Metodo3();
    Console.WriteLine("Ejecución exitosa de Método 3");
}

private static void Metodo3()
{
    Console.WriteLine("Método 3 >> Llamando al Método 4");
    Metodo4();
    Console.WriteLine("Ejecución exitosa de Método 4");
}

private static void Metodo4()
{
    Console.WriteLine("Método 4 >> Llamando al Método 5");
    Metodo5();
    Console.WriteLine("Ejecución exitosa de Método 5");
}

private static void Metodo5()
{
    try
    {
        Console.WriteLine("Método 5");
        throw new Exception("Exception de prueba de bubbling");
    }
    catch (Exception ex)
    {
        Console.WriteLine("\nHa ocurrido una excepción en el Método 5:\n\n" + ex);
        // Manejo del error impiendo el bubbling, idealmente log del error
    }
    finally
    {
        Console.WriteLine("\nEjecutado bloque finally del método 5.");
    }
}

Como puede verse el StackTrace se reduce considerablemente pues el bubling de la excepción se detiene en el mismo método en el que se produce el error (siendo claro, en este caso, del todo, no habría bubbling), además se constata que los temas métodos completan su ejecución, finalmente el catch del método errorbubbling no se ejecuta, pero si todos los bloques finally.

El costo de los errores (malas prácticas)


¿Hay una penalización de rendimiento por el manejo de los errores? Por supuesto, que desde luego, que claro, que obviamente que sí…

El manejo de los errores tiene un costo significativo, por ejemplo una práctica que puede volverse común es utilizar el try…cacth de la siguiente manera:

            try
            {
               // Código a ejecutar
            }
            catch (Exception ex)
            {
                throw ex;
            }      
O bien esta otra variante:

            try
            {
               // Código a ejecutar
            }
            catch (Exception ex)
            {
                throw;
            }     
Lo que provocará es que el error sea relanzado, no por el .Net Framework, si no, explícitamente por el  código del desarrollador. Utilizando la primer práctica el código de nuestro ejemplo queda de la siguiente manera:

private static void errorbubbling()
{
    try
    {
        Console.WriteLine("Inicia ejecución >> Llamando a Método 1");
        Metodo1();
        Console.WriteLine("Ejecución exitosa de Método 1");
    }
    catch (Exception ex)
    {
        Console.WriteLine("\nHa ocurrido una excepción:\n\n" + ex);
    }
    finally
    {
        Console.WriteLine("\nEjecutado bloque finally.");
        Console.ReadKey();
    }
}

private static void Metodo1()
{
    try
    {
        Console.WriteLine("Método 1 >> Llamando al Método 2");
        Metodo2();
        Console.WriteLine("Ejecución exitosa de Método 2");
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

private static void Metodo2()
{
    try
    {
        Console.WriteLine("Método 2 >> Llamando al Método 3");
        Metodo3();
        Console.WriteLine("Ejecución exitosa de Método 3");
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

private static void Metodo3()
{
    try
    {
        Console.WriteLine("Método 3 >> Llamando al Método 4");
        Metodo4();
        Console.WriteLine("Ejecución exitosa de Método 4");
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

private static void Metodo4()
{
    try
    {
        Console.WriteLine("Método 4 >> Llamando al Método 5");
        Metodo5();
        Console.WriteLine("Ejecución exitosa de Método 5");
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

private static void Metodo5()
{
    try
    {
        Console.WriteLine("Método 5");
        throw new Exception("Exception de prueba de bubbling");
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

Usando la segunda practica queda de la siguiente manera:

private static void errorbubbling()
{
    try
    {
        Console.WriteLine("Inicia ejecución >> Llamando a Método 1");
        Metodo1();
        Console.WriteLine("Ejecución exitosa de Método 1");
    }
    catch (Exception ex)
    {
        Console.WriteLine("\nHa ocurrido una excepción:\n\n" + ex);
    }
    finally
    {
        Console.WriteLine("\nEjecutado bloque finally.");
        Console.ReadKey();
    }
}

private static void Metodo1()
{
    try
    {
        Console.WriteLine("Método 1 >> Llamando al Método 2");
        Metodo2();
        Console.WriteLine("Ejecución exitosa de Método 2");
    }
    catch (Exception ex)
    {
        throw;
    }
}

private static void Metodo2()
{
    try
    {
        Console.WriteLine("Método 2 >> Llamando al Método 3");
        Metodo3();
        Console.WriteLine("Ejecución exitosa de Método 3");
    }
    catch (Exception ex)
    {
        throw;
    }
}

private static void Metodo3()
{
    try
    {
        Console.WriteLine("Método 3 >> Llamando al Método 4");
        Metodo4();
        Console.WriteLine("Ejecución exitosa de Método 4");
    }
    catch (Exception ex)
    {
        throw;
    }
}

private static void Metodo4()
{
    try
    {
        Console.WriteLine("Método 4 >> Llamando al Método 5");
        Metodo5();
        Console.WriteLine("Ejecución exitosa de Método 5");
    }
    catch (Exception ex)
    {
        throw;
    }
}

private static void Metodo5()
{
    try
    {
        Console.WriteLine("Método 5");
        throw new Exception("Exception de prueba de bubbling");
    }
    catch (Exception ex)
    {
        throw;
    }
}


Como puede verse, no es inocua la elección entre un método u otro. Si usamos throw ex; el StackTrace se reinicia con cada llamada, perdiéndose la información previa del error, cosa que no sucede si únicamente usamos throw; por lo que, generalmente, es preferible usar throw; en lugar de throw ex;.

Llegados a este punto nos preguntamos ¿y el costo? Pues bien realizamos algunas pruebas. En los siguientes pruebas se eliminaron todos los console.write dentro de los métodos para controlar solo el manejo de errores, los resultados son los siguientes:





 

Conclusiones

- El costo del manejo de las excepciones es bastante elevado.
- Es preferible manejar el error en el método en el que se produce el error y evitar el bubbling.
- Si el punto anterior no es viable, la regla es no usar catch a menos que se vaya a tratar el error apropiadamente, es mejor evitar el try catch si el error se va a manejar en el método que llama al actual, es decir, si solo se va usar throw dentro del catch, es mejor no hacerlo.
- Entre throw; y throw ex; es mejor elegir thow; ya que mantiene el StackTrace.
- No olvidar registrar la excepción en una bitácora o en el visor de eventos para examinar apropiadamente el problema que lo produce.
- Siempre es preferible no lanzar un error.

Gracias a nuestro"kohāi" Brayan por señalarnos que estábamos reproduciendo un código ineficiente en alguno de los manejos de excepciones. Siempre es importante hacer retrospectivas y atender a las sugerencias para mejorar.