CodeGym /Cursos /C# SELF /Pila de llamadas y creación de excepciones propias

Pila de llamadas y creación de excepciones propias

C# SELF
Nivel 13 , Lección 3
Disponible

1. Conociendo la pila de llamadas (Call Stack)

En la lección pasada mencionamos brevemente la pila de llamadas, ahora vamos a verla más a fondo.

Imagínate: el usuario pulsa un botón en la app. Ese botón llama al método OnClick() ("Botón pulsado"). Ese, a su vez, llama a LoadData() (cargar datos), y ese — a ReadFromFile() (leer de archivo).

De repente, en ReadFromFile() ocurre un error — archivo no encontrado. ¿Quién tiene la culpa?

Para averiguarlo, el programa va "hacia atrás en las huellas": de ReadFromFile() → a LoadData() → a OnClick(). Ese camino es lo que se llama pila de llamadas — como una pila de platos, donde el último arriba cae primero.

El programa baja por esa pila hasta encontrar un catch adecuado, pero ejecuta todos los finally que hay en el camino — para que todo quede cerrado, liberado y ordenado.

En programación, la pila de llamadas es una lista que recuerda quién llamó a quién, para que, si pasa algo, puedas recorrer ese "camino de llamadas" y buscar la causa del fallo.

Cómo funciona

Cuando se ejecuta el programa, cada llamada a un método (o función) añade una nueva "línea" a la pila (lista) de llamadas. Si durante la ejecución ocurre una excepción, la clase .NET Exception guarda información sobre esa pila: dónde y en qué orden se llamaron los métodos que llevaron al error.

2. ¿Para qué sirve la pila de llamadas?

La pila de llamadas es tu mejor colega cuando depuras ("debugueas") errores complicados.

  • Te muestra no solo qué pasó, sino también dónde exactamente y quién fue el culpable.
  • A veces, mirando la pila, te sorprendes de cómo el programa llegó a ese estado (sobre todo si alguien pasó por error un valor de argumento equivocado).

Historia típica: Imagina que tienes un proyecto enorme, donde los métodos se llaman unos a otros en diez niveles. De repente aparece un NullReferenceException, pero no entiendes cómo llegó el programa ahí. Abres la pila — ves toda la cadena de llamadas, y ya es mucho más fácil saber por dónde empezar a investigar.

Ejemplo:

class MyClass 
{
    public void MethodA()     { MethodB(); }
    
    public void MethodB()     { MethodC(); }
    
    public void MethodC()     {
        throw new Exception("¡Error!");
    }    

    public void Main() 
    {
        try 
        { 
            MethodA(); 
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.StackTrace);
        }        
    }
}

3. Cómo crear tus propias excepciones

A veces las excepciones estándar no bastan

En .NET hay un montón de excepciones estándar (ArgumentNullException, InvalidOperationException, etc.), pero a veces no es suficiente:

  • En tu app tienes tus propias "reglas del juego": por ejemplo, que el usuario no puede comprar más de 10 productos de una vez, o que en tu lógica de negocio no debe haber sumas negativas.
  • Quieres separar los errores de lógica de aplicación de los errores "de sistema".

Ahí es cuando te entran ganas de crear "la tuya" — ¡ya que se puede!


using System;

// Excepción propia: usuario no encontrado
public class UserNotFoundException : Exception
{
    // constructor base
    public UserNotFoundException() : base("Usuario no encontrado.") { }

    // constructor con mensaje
    public UserNotFoundException(string message) : base(message) { }

    // constructor con mensaje y excepción interna
    public UserNotFoundException(string message, Exception inner) : base(message, inner) { }
}
Ejemplo de excepción propia UserNotFoundException

Breve sobre los constructores

  • Sin parámetros — pone un mensaje estándar
  • Con tu propio mensaje — a veces quieres añadir detalles
  • Con excepción interna (inner) — para que, si el error pasó "dentro de otro error", no pierdas info importante.

Cómo usar tu excepción

Supón que modelamos un método de búsqueda de usuario en nuestra app de gestión de tareas:

using System;

public class UserService
{
    public string FindUserNameById(int userId)
    {
        // "Buscamos" usuario, si no se encuentra — lanzamos excepción
        if (userId != 42)
            throw new UserNotFoundException($"Usuario con id {userId} no encontrado.");

        return "Maxim";
    }
}

En el programa principal:

// En Main
var service = new UserService();
try
{
    string name = service.FindUserNameById(17);
    Console.WriteLine("Nombre de usuario: " + name);
}
catch (UserNotFoundException ex)
{
    Console.WriteLine("Problema buscando usuario: " + ex.Message);
    // La pila de llamadas también está disponible aquí con ex.StackTrace
}

Si pasamos un id distinto de 42, obtenemos:

Problema buscando usuario: Usuario con id 17 no encontrado.

4. Razones para crear tus propias excepciones

Logging y separación de errores

Supón que en tu app hay un montón de errores distintos, y necesitas tratarlos de forma diferente. Por ejemplo, los errores de base de datos los logueas como "fatales", los de usuario — se los muestras al usuario en rojo, y los de red — intentas repetir la operación. Agrupar por tipo de excepción es una forma genial de hacerlo.

OOP y herencia

Puedes organizar una jerarquía de errores de tu dominio:

public class MyAppException : Exception { ... }
public class OrderException : MyAppException { ... }
public class ProductException : MyAppException { ... }
public class TooManyItemsInOrderException : OrderException { ... }

Ahora, si capturas MyAppException, manejas todos los errores de tu dominio, y si quieres una reacción especial para "pedido demasiado grande" — capturas el caso más concreto.

5. Qué tener en cuenta al crear excepciones propias

  • No lances excepciones solo por lanzarlas
    Crea excepciones propias solo cuando:
    • Realmente ayuda a que el código sea más claro
    • Hay posibilidad de que las capturen desde el código que llama
    • Quieres dar más info al código que llama (con campos/propiedades)
  • Buena práctica: Serialización
    En .NET las excepciones estándar soportan serialización (para enviar por red, por ejemplo). En apps simples casi nunca hace falta, pero para casos "avanzados" — añade el atributo [Serializable] y un constructor para serialización (ver documentación oficial). Para ejemplos de clase no hace falta, pero en el curro — pregunta a tu team lead :)
    Serialización — es el proceso de convertir un objeto a un formato cómodo para guardar o enviar (por ejemplo, a un archivo o por red). Veremos serialización más adelante.

6. Particularidades de la pila de llamadas: dónde puede haber lío

La pila de llamadas solo muestra el camino hasta el punto donde ocurrió la excepción. Si capturas una excepción y lanzas una nueva sin indicar la excepción "interna" (el constructor Exception(string, Exception inner)), puedes perder el origen del error. A esto se le llama "ocultar la pila".

Mal:

try
{
    // Aquí ocurre algún error
}
catch (Exception ex)
{
    throw new Exception("Ocurrió un error desconocido."); // ¡Se pierde la pila del error anterior!
}

Bien:

try
{
    // Aquí ocurre algún error
}
catch (Exception ex)
{
    throw new Exception("Ocurrió un error desconocido.", ex); // ¡Guardamos la pila original!
}

Así en StackTrace queda tanto el camino al primer error como tu nuevo mensaje.

7. Consejos prácticos y errores comunes

  • No lances excepciones para controlar la lógica normal (por ejemplo, para "salir de un bucle" — ¡hay formas más elegantes!).
  • Captura el tipo de excepción que necesitas — capturar siempre Exception no siempre es buena idea (puedes "tragarte" un error que no deberías).
  • Siempre loguea la pila de llamadas en errores complicados. Te ahorrará un montón de nervios buscando bugs.
  • Usa excepciones internas — ayuda a no perder info sobre la causa raíz del fallo.
  • Describe tus excepciones claramente — para que quien lea el código dentro de un año entienda por qué salió ese error.
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION