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) { }
}
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.
GO TO FULL VERSION