Módulo 09: Manejo de errores

Objetivos del módulo

  • Comprender qué son las excepciones y por qué ocurren
  • Dominar try, catch, finally
  • Crear excepciones personalizadas
  • Aplicar buenas prácticas en el manejo de errores

Depuración de excepciones en Visual Studio Community

Visual Studio Community incluye un depurador muy potente que te ayuda a inspeccionar excepciones paso a paso. Usa F9 para colocar puntos de interrupción (breakpoints) y F10/F11 para recorrer el código mientras inspeccionas el valor de las variables:

Depuración de excepciones en Visual Studio Panel de depuración de Visual Studio: breakpoints, variables locales y configuración de excepciones

💡 Consejo: Abre Depurar → Ventanas → Configuración de excepciones (Ctrl+Alt+E) para indicar a VS que se detenga automáticamente cuando se lance un tipo concreto de excepción, incluso si está dentro de un catch.


1. ¿Qué es una excepción?

📘 Concepto: Una excepción es un error que ocurre durante la ejecución del programa (no al compilar). Cuando ocurre una excepción que no está controlada, el programa se detiene abruptamente.

Ejemplos comunes

// DivideByZeroException
int resultado = 10 / 0;

// FormatException
int numero = int.Parse("abc");

// IndexOutOfRangeException
int[] arr = { 1, 2, 3 };
Console.WriteLine(arr[10]);

// NullReferenceException
string? texto = null;
Console.WriteLine(texto.Length);

// FileNotFoundException
string contenido = File.ReadAllText("archivo_inexistente.txt");

Si no capturas estas excepciones, el programa se cierra mostrando un mensaje de error al usuario.


2. try / catch: capturar excepciones

La estructura try-catch permite capturar una excepción y manejarla en lugar de que el programa se cierre:

try
{
    Console.Write("Introduce un número: ");
    int numero = int.Parse(Console.ReadLine()!);
    int resultado = 100 / numero;
    Console.WriteLine($"100 / {numero} = {resultado}");
}
catch (FormatException)
{
    Console.WriteLine("Error: no has introducido un número válido.");
}
catch (DivideByZeroException)
{
    Console.WriteLine("Error: no se puede dividir entre cero.");
}
Parte Función
try { } Contiene el código que podría lanzar una excepción
catch (TipoExcepcion) { } Se ejecuta solo si ocurre una excepción de ese tipo

Capturar la información de la excepción

try
{
    int[] nums = { 1, 2, 3 };
    Console.WriteLine(nums[10]);
}
catch (IndexOutOfRangeException ex)
{
    Console.WriteLine($"Error: {ex.Message}");
    // "Error: Index was outside the bounds of the array."
}

Capturar cualquier excepción

try
{
    // código peligroso
}
catch (Exception ex)  // Exception es la clase base de todas las excepciones
{
    Console.WriteLine($"Ha ocurrido un error: {ex.Message}");
}

⚠️ Importante: Capturar Exception (la clase base) captura cualquier excepción. Úsalo solo como último recurso. Es mejor capturar excepciones específicas primero.

Orden de los catch

Los bloques catch se evalúan de más específico a más general:

try
{
    // código
}
catch (FileNotFoundException ex)
{
    Console.WriteLine($"Archivo no encontrado: {ex.FileName}");
}
catch (IOException ex)
{
    Console.WriteLine($"Error de entrada/salida: {ex.Message}");
}
catch (Exception ex)
{
    Console.WriteLine($"Error inesperado: {ex.Message}");
}

3. finally: código que siempre se ejecuta

📘 Concepto: El bloque finally se ejecuta siempre, tanto si hubo excepción como si no. Es ideal para liberar recursos (cerrar archivos, conexiones, etc.).

StreamReader? lector = null;

try
{
    lector = new StreamReader("datos.txt");
    string contenido = lector.ReadToEnd();
    Console.WriteLine(contenido);
}
catch (FileNotFoundException)
{
    Console.WriteLine("El archivo no existe.");
}
catch (IOException ex)
{
    Console.WriteLine($"Error al leer: {ex.Message}");
}
finally
{
    // Se ejecuta SIEMPRE
    lector?.Close();  // ?. → solo llama a Close si lector no es null
    Console.WriteLine("Recurso liberado.");
}

4. Lanzar excepciones: throw

Puedes lanzar tus propias excepciones cuando detectas un estado inválido:

static double CalcularIMC(double peso, double altura)
{
    if (peso <= 0)
        throw new ArgumentException("El peso debe ser mayor que 0", nameof(peso));

    if (altura <= 0)
        throw new ArgumentException("La altura debe ser mayor que 0", nameof(altura));

    return peso / (altura * altura);
}

try
{
    double imc = CalcularIMC(70, 0);
}
catch (ArgumentException ex)
{
    Console.WriteLine($"Datos inválidos: {ex.Message}");
    // "Datos inválidos: La altura debe ser mayor que 0 (Parameter 'altura')"
}

Re-lanzar una excepción

A veces quieres registrar el error y después re-lanzarlo para que el código superior lo maneje:

try
{
    // operación riesgosa
}
catch (Exception ex)
{
    Console.WriteLine($"Log: {ex.Message}");  // Registrar el error
    throw;  // Re-lanzar la misma excepción (conserva la traza)
}

⚠️ Importante: Usa throw; (sin especificar la excepción) para re-lanzar. Si usas throw ex; se pierde la traza de pila original, dificultando la depuración.


5. Excepciones personalizadas

Para errores específicos de tu aplicación, puedes crear tus propias excepciones:

// Excepción personalizada
class SaldoInsuficienteException : Exception
{
    public double SaldoActual { get; }
    public double MontoSolicitado { get; }

    public SaldoInsuficienteException(double saldo, double monto)
        : base($"Saldo insuficiente. Tienes {saldo:C2} y necesitas {monto:C2}")
    {
        SaldoActual = saldo;
        MontoSolicitado = monto;
    }
}

// Uso
class CuentaBancaria
{
    public double Saldo { get; private set; }

    public CuentaBancaria(double saldoInicial)
    {
        Saldo = saldoInicial;
    }

    public void Retirar(double monto)
    {
        if (monto <= 0)
            throw new ArgumentException("El monto debe ser positivo");

        if (monto > Saldo)
            throw new SaldoInsuficienteException(Saldo, monto);

        Saldo -= monto;
        Console.WriteLine($"Retirado: {monto:C2}. Saldo: {Saldo:C2}");
    }
}

// Programa
var cuenta = new CuentaBancaria(100);

try
{
    cuenta.Retirar(50);   // OK
    cuenta.Retirar(80);   // Error: solo tiene 50
}
catch (SaldoInsuficienteException ex)
{
    Console.WriteLine(ex.Message);
    Console.WriteLine($"Te faltan: {ex.MontoSolicitado - ex.SaldoActual:C2}");
}

6. Jerarquía de excepciones comunes

classDiagram
    Exception <|-- SystemException
    SystemException <|-- ArgumentException
    SystemException <|-- NullReferenceException
    SystemException <|-- InvalidOperationException
    SystemException <|-- IOException
    SystemException <|-- IndexOutOfRangeException
    SystemException <|-- DivideByZeroException
    SystemException <|-- FormatException
    ArgumentException <|-- ArgumentNullException
    ArgumentException <|-- ArgumentOutOfRangeException
    IOException <|-- FileNotFoundException
    IOException <|-- DirectoryNotFoundException
    Exception <|-- ApplicationException
    ApplicationException <|-- SaldoInsuficienteException
Excepción Cuándo ocurre
ArgumentException Argumento no válido
ArgumentNullException Argumento null cuando no debería
ArgumentOutOfRangeException Argumento fuera de rango
NullReferenceException Acceso a miembro de un objeto null
InvalidOperationException Operación no válida en el estado actual
DivideByZeroException División entre cero
FormatException Formato de cadena no válido al convertir
IndexOutOfRangeException Índice fuera de los límites del array
FileNotFoundException Archivo no encontrado
IOException Error general de entrada/salida
OverflowException Desbordamiento numérico
StackOverflowException Recursión infinita (no se puede capturar)

7. Filtros de excepción: when

Puedes añadir condiciones extra a los bloques catch:

try
{
    // Operación HTTP simulada
    int statusCode = 404;
    throw new HttpRequestException($"Error HTTP {statusCode}");
}
catch (HttpRequestException ex) when (ex.Message.Contains("404"))
{
    Console.WriteLine("Recurso no encontrado (404)");
}
catch (HttpRequestException ex) when (ex.Message.Contains("500"))
{
    Console.WriteLine("Error del servidor (500)");
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"Otro error HTTP: {ex.Message}");
}

8. Buenas prácticas

✅ Haz esto

// 1. Captura excepciones específicas
catch (FileNotFoundException ex) { ... }

// 2. Usa TryParse en lugar de Parse + catch
if (int.TryParse(input, out int numero))
{
    // usar numero
}

// 3. Valida antes de operar (evita la excepción)
if (divisor != 0)
{
    resultado = dividendo / divisor;
}

// 4. Usa 'using' para recursos (cierra automáticamente)
using StreamReader reader = new StreamReader("archivo.txt");
string contenido = reader.ReadToEnd();
// reader se cierra automáticamente al salir del ámbito

❌ No hagas esto

// 1. NO captures Exception genérica sin motivo
catch (Exception) { }  // Se "traga" el error, oculta bugs

// 2. NO uses excepciones para flujo normal
try
{
    int i = 0;
    while (true)  // ¡TERRIBLE!
        Console.WriteLine(array[i++]);
}
catch (IndexOutOfRangeException) { }
// Usa: for (int i = 0; i < array.Length; i++)

// 3. NO lances Exception genérica
throw new Exception("algo salió mal");  // Poco informativo
// Mejor: throw new InvalidOperationException("No se puede...");

// 4. NO pierdas la traza de pila
catch (Exception ex)
{
    throw ex;   // ¡MAL! Pierde la traza de pila
    throw;      // BIEN: conserva la traza original
}

9. Ejercicios

Ejercicio 1: Calculadora robusta

Mejora la calculadora del módulo anterior: maneja todas las posibles excepciones (formato inválido, división por cero, overflow). Muestra mensajes claros al usuario y permite reintentar.

Ejercicio 2: Registro de usuarios

Crea una función RegistrarUsuario(string nombre, string email, int edad) que lance:

  • ArgumentNullException si nombre o email son null/vacío
  • ArgumentOutOfRangeException si la edad no está entre 18 y 120
  • FormatException si el email no contiene @

Captura cada excepción con mensajes apropiados.

Ejercicio 3: Lectura segura de archivo

Pide al usuario un nombre de archivo y muestra su contenido. Maneja: FileNotFoundException, IOException, UnauthorizedAccessException. Usa finally para confirmar que se liberaron los recursos.

Ejercicio 4: Banco con excepciones

Expande el ejemplo de CuentaBancaria:

  • Añade transferencias entre cuentas
  • Crea excepciones CuentaNoEncontradaException y LimiteTransferenciaException
  • Implementa un historial de transacciones

Resumen

Concepto Sintaxis / Ejemplo
try-catch try { } catch (Exception ex) { }
finally try { } catch { } finally { }
Lanzar excepción throw new ArgumentException("msg");
Re-lanzar throw; (sin variable)
Excepción personalizada class MiError : Exception { }
Filtro when catch (Exception ex) when (condición)
TryParse (evitar excepción) int.TryParse(s, out int n)
using (auto-dispose) using var f = new StreamReader(...)