En las lecciones anteriores has aprendido a tomar decisiones, a repetir acciones y a clasificar valores, pero siempre asumiendo que los datos llegan correctos: un número que realmente es un número, un índice que existe dentro del array, un libro que sí está en el catálogo. En un programa real, eso no está garantizado: un usuario puede escribir texto donde se espera un número, se puede intentar prestar un libro que no existe, o dividir por un valor que resulta ser cero. Cuando algo así ocurre, C# no continúa como si nada: lanza una excepción, un objeto especial que interrumpe el flujo normal del programa para señalar que algo ha ido mal. Esta lección, la última del Módulo 2, te enseña a anticipar esas situaciones y a gestionarlas de forma controlada con try/catch/finally, en lugar de dejar que el programa se detenga bruscamente.

Contenido

  1. ¿Qué es una excepción?
  2. try/catch: capturar errores
  3. finally: código que siempre se ejecuta
  4. Lanzar excepciones propias con throw
  5. Tipos de excepción comunes
  6. Capturar múltiples tipos de excepción
  7. Buenas prácticas con excepciones
  8. Aplicación completa: prestar un libro de forma segura

  1. ¿Qué es una excepción?

Una excepción es un objeto (de una clase que deriva, directa o indirectamente, de System.Exception) que representa un error ocurrido durante la ejecución del programa. Si no se hace nada para gestionarla, la excepción se propaga hacia arriba hasta que, si nadie la captura, el programa termina de forma abrupta y muestra en consola un mensaje de error junto con la pila de llamadas (stack trace): la secuencia de métodos que se estaban ejecutando cuando ocurrió el fallo.

int[] copiasDisponibles = { 3, 0, 5 };

Console.WriteLine(copiasDisponibles[10]); // IndexOutOfRangeException: el programa se detiene aqui
Console.WriteLine("Esta linea nunca se ejecuta");

Al ejecutar este código, la consola mostraría algo similar a:

Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at Program.Main(String[] args)

El objetivo de esta lección es evitar que errores previsibles como este detengan BiblioTechConsola por completo, y en su lugar reaccionar a ellos de forma controlada.

  1. try/catch: capturar errores

El bloque try envuelve el código que podría fallar; el bloque catch define qué hacer si efectivamente falla.

int[] copiasDisponibles = { 3, 0, 5 };

try
{
    Console.WriteLine(copiasDisponibles[10]);
}
catch (IndexOutOfRangeException)
{
    Console.WriteLine("Error: se ha intentado acceder a una posicion que no existe en el array.");
}

Console.WriteLine("El programa continua ejecutandose con normalidad.");

A diferencia del ejemplo anterior, aquí el programa no se detiene: la excepción se captura, se muestra un mensaje comprensible, y la ejecución continúa en la línea siguiente al bloque try/catch.

También se puede acceder al objeto de la excepción para consultar información adicional, como su propiedad Message:

try
{
    string entradaUsuario = "no-es-un-numero";
    int paginas = int.Parse(entradaUsuario);
    Console.WriteLine($"Paginas: {paginas}");
}
catch (FormatException ex)
{
    Console.WriteLine($"Error al convertir el texto a numero: {ex.Message}");
}

  1. finally: código que siempre se ejecuta

El bloque finally, opcional, contiene código que se ejecuta siempre, tanto si el bloque try termina con éxito como si se lanza y captura una excepción. Es el lugar habitual para liberar recursos (cerrar un fichero, una conexión de red o de base de datos) que deben cerrarse ocurra lo que ocurra.

try
{
    Console.WriteLine("Abriendo el registro de prestamos...");
    int paginas = int.Parse("120");
    Console.WriteLine($"Registro procesado: {paginas} paginas");
}
catch (FormatException ex)
{
    Console.WriteLine($"Error de formato: {ex.Message}");
}
finally
{
    Console.WriteLine("Cerrando el registro de prestamos.");
}

En este ejemplo, "Cerrando el registro de prestamos." se muestra siempre: tanto si la conversión de "120" tiene éxito, como si fallara y el catch capturase el error. Verás finally con mucho más protagonismo en el Módulo 5, al trabajar con ficheros y bases de datos, donde cerrar correctamente los recursos abiertos es crítico.

  1. Lanzar excepciones propias con throw

Además de capturar excepciones que lanza el propio entorno de ejecución (.NET), tu código puede lanzar sus propias excepciones con throw, para señalar una situación anómala según las reglas de negocio de BiblioTech.

void RegistrarPrestamo(bool libroDisponible)
{
    if (!libroDisponible)
    {
        throw new InvalidOperationException("No se puede prestar un libro que no esta disponible.");
    }

    Console.WriteLine("Prestamo registrado correctamente.");
}

try
{
    RegistrarPrestamo(libroDisponible: false);
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"Operacion no valida: {ex.Message}");
}

throw new TipoDeExcepcion("mensaje descriptivo") crea una nueva instancia de la excepción indicada y la lanza inmediatamente, interrumpiendo la ejecución normal del método hasta que algún bloque catch (en este método o en quien lo llame) la capture.

  1. Tipos de excepción comunes

.NET ofrece muchas clases de excepción ya preparadas para situaciones habituales. Usar el tipo más específico posible (en lugar de la genérica Exception) ayuda a quien lee el código —y a quien lo captura— a entender exactamente qué salió mal.

Excepción Cuándo se produce (o se lanza a propósito)
Exception Clase base de la que derivan todas las demás; representa "un error genérico"
ArgumentException Se pasó un argumento no válido a un método
ArgumentNullException Se pasó null donde se esperaba un valor (deriva de ArgumentException)
InvalidOperationException Se intenta una operación que no es válida en el estado actual del objeto/programa
FormatException Una cadena de texto no tiene el formato esperado para convertirla (por ejemplo, int.Parse sobre texto no numérico)
IndexOutOfRangeException Se accede a una posición de un array fuera de sus límites
NullReferenceException Se intenta usar un miembro (método, propiedad) de una variable que vale null
DivideByZeroException Se realiza una división entera entre cero
void ValidarNumeroPaginas(int paginas)
{
    if (paginas <= 0)
    {
        throw new ArgumentException("El numero de paginas debe ser mayor que cero.", nameof(paginas));
    }
}

try
{
    ValidarNumeroPaginas(-50);
}
catch (ArgumentException ex)
{
    Console.WriteLine($"Argumento invalido: {ex.Message}");
}

  1. Capturar múltiples tipos de excepción

Un mismo bloque try puede tener varios bloques catch, cada uno para un tipo distinto de excepción. C# evalúa los bloques catch de arriba a abajo y ejecuta el primero cuyo tipo coincida (o sea una clase base) con la excepción lanzada, así que los tipos más específicos deben ir siempre antes que los más genéricos.

string[] titulosLibros = { "Cien anos de soledad", "Rayuela", "Ficciones" };

try
{
    Console.Write("Introduce la posicion del libro que quieres consultar: ");
    string? entrada = Console.ReadLine();
    int posicion = int.Parse(entrada!);

    Console.WriteLine(titulosLibros[posicion]);
}
catch (FormatException)
{
    Console.WriteLine("Debes introducir un numero entero valido.");
}
catch (IndexOutOfRangeException)
{
    Console.WriteLine("No existe ningun libro en esa posicion del catalogo.");
}
catch (Exception ex)
{
    Console.WriteLine($"Ha ocurrido un error inesperado: {ex.Message}");
}

Si FormatException y IndexOutOfRangeException estuvieran después de catch (Exception ex), nunca se ejecutarían: el compilador de C# directamente marca ese orden como un error, porque un bloque catch (Exception ex) previo capturaría ya cualquier excepción, dejando el resto de bloques inalcanzables.

También es posible capturar varios tipos con un mismo bloque, usando el operador when para añadir una condición adicional, o simplemente definiendo el bloque para la clase base común que interese; para la mayoría de casos cotidianos, sin embargo, un catch por tipo (como en el ejemplo anterior) es más claro.

  1. Buenas prácticas con excepciones

Buena práctica Por qué importa
Capturar el tipo más específico posible Facilita dar un mensaje preciso y no oculta errores de otro tipo
No usar excepciones para el flujo normal del programa Son costosas en rendimiento y dificultan seguir la lógica del programa
No capturar Exception genérica "porque sí" Puede ocultar errores de programación reales que deberían solucionarse
Incluir mensajes descriptivos en throw new ...(...) Ayuda a diagnosticar el problema sin tener que adivinar la causa
Usar finally (o recursos que se cierran solos) para liberar recursos Evita fugas de memoria, ficheros bloqueados o conexiones abiertas

Sobre el primer punto de la tabla —no usar excepciones para el flujo normal—: si una situación es previsible y frecuente (por ejemplo, comprobar si un texto se puede convertir a número), suele ser mejor usar herramientas como int.TryParse (que ya viste en la lección de variables del Módulo 1) en lugar de envolver int.Parse en un try/catch y esperar a que falle. Las excepciones están pensadas para situaciones excepcionales, no para validaciones rutinarias:

// Preferible para validaciones rutinarias: sin excepciones de por medio
string entrada = "abc";
if (int.TryParse(entrada, out int paginas))
{
    Console.WriteLine($"Numero de paginas: {paginas}");
}
else
{
    Console.WriteLine("Entrada no valida.");
}

// Reservar try/catch para lo verdaderamente inesperado o fuera de tu control,
// como el acceso a un array con una posicion que en teoria nunca deberia ocurrir.

  1. Aplicación completa: prestar un libro de forma segura

Cerramos la lección —y el módulo— con un ejemplo que combina varias de las ideas vistas: buscar un libro por título, validar la entrada del usuario, y lanzar y capturar excepciones propias cuando la operación de préstamo no es posible.

string[] titulosLibros = { "Cien anos de soledad", "Rayuela", "Ficciones" };
bool[] disponibilidad = { true, false, true };

void PrestarLibro(string tituloBuscado)
{
    int posicionEncontrada = -1;

    for (int i = 0; i < titulosLibros.Length; i++)
    {
        if (titulosLibros[i] == tituloBuscado)
        {
            posicionEncontrada = i;
            break;
        }
    }

    if (posicionEncontrada == -1)
    {
        throw new ArgumentException($"El libro '{tituloBuscado}' no existe en el catalogo.");
    }

    if (!disponibilidad[posicionEncontrada])
    {
        throw new InvalidOperationException($"El libro '{tituloBuscado}' ya esta prestado.");
    }

    disponibilidad[posicionEncontrada] = false;
    Console.WriteLine($"Prestamo de '{tituloBuscado}' registrado correctamente.");
}

string[] solicitudes = { "Rayuela", "1984", "Ficciones" };

foreach (string solicitud in solicitudes)
{
    try
    {
        PrestarLibro(solicitud);
    }
    catch (ArgumentException ex)
    {
        Console.WriteLine($"No se pudo procesar la solicitud: {ex.Message}");
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine($"No se pudo procesar la solicitud: {ex.Message}");
    }
    finally
    {
        Console.WriteLine($"--- Fin del procesamiento de '{solicitud}' ---");
    }
}

Salida esperada:

No se pudo procesar la solicitud: El libro 'Rayuela' ya esta prestado.
--- Fin del procesamiento de 'Rayuela' ---
No se pudo procesar la solicitud: El libro '1984' no existe en el catalogo.
--- Fin del procesamiento de '1984' ---
Prestamo de 'Ficciones' registrado correctamente.
--- Fin del procesamiento de 'Ficciones' ---

Observa cómo el bucle foreach sigue procesando todas las solicitudes aunque alguna de ellas falle: gracias al try/catch dentro del propio bucle, un error en una solicitud no impide que se procesen las siguientes, y el finally garantiza que el mensaje de cierre se muestre en los tres casos.

Errores Comunes y Consejos

  • Capturar Exception genérica de forma sistemática: oculta el tipo real de error y dificulta diagnosticar problemas. Resérvalo como último recurso, después de los tipos específicos, o para registrar errores verdaderamente inesperados.
  • Usar excepciones para validaciones que se pueden anticipar: si puedes comprobar la condición antes con un if o con TryParse, hazlo; no dejes que el programa falle "a propósito" para luego capturarlo.
  • Bloques catch vacíos: capturar una excepción y no hacer nada con ella (ni mostrar un mensaje, ni registrarla) esconde errores que deberían resolverse; como mínimo, informa del problema.
  • Orden incorrecto de los catch: los tipos más específicos siempre antes que los más genéricos; en caso contrario, el compilador marcará el bloque genérico como inalcanzable.
  • Olvidar liberar recursos: si abres algo que debe cerrarse (ficheros, conexiones), usa finally o las construcciones que ya se encargan de ello automáticamente (lo verás con using en el Módulo 5, al trabajar con ficheros).
  • Mensajes de excepción poco descriptivos: throw new Exception("Error") no ayuda a nadie a entender qué pasó; incluye siempre datos relevantes (qué libro, qué valor, qué operación).

Ejercicios

  1. Escribe un bloque try/catch que intente convertir la cadena "cien" a int con int.Parse, capture la excepción FormatException y muestre por consola un mensaje indicando que la entrada no es un número válido.

  2. Crea una función local void ValidarISBN(string isbn) que lance una ArgumentException con el mensaje "El ISBN no puede estar vacio." si el parámetro isbn es una cadena vacía (""), y que en caso contrario muestre por consola "ISBN valido.". Llama a la función con "" dentro de un try/catch que capture la excepción y muestre su mensaje.

  3. Dado el array int[] copiasDisponibles = { 2, 0, 3 };, escribe un try/catch/finally que intente acceder a la posición 5 del array (que no existe), capture la excepción IndexOutOfRangeException mostrando un mensaje adecuado, y en el finally muestre "Consulta finalizada." independientemente de si hubo error o no.

Soluciones

try
{
    int numero = int.Parse("cien");
    Console.WriteLine(numero);
}
catch (FormatException)
{
    Console.WriteLine("La entrada no es un numero valido.");
}

Como "cien" no se puede convertir a int, se lanza FormatException y el bloque catch muestra el mensaje "La entrada no es un numero valido."; el programa continúa con normalidad después del bloque.

void ValidarISBN(string isbn)
{
    if (isbn == "")
    {
        throw new ArgumentException("El ISBN no puede estar vacio.");
    }

    Console.WriteLine("ISBN valido.");
}

try
{
    ValidarISBN("");
}
catch (ArgumentException ex)
{
    Console.WriteLine(ex.Message);
}

Al llamar a ValidarISBN(""), se lanza la excepción con el mensaje indicado, que el catch captura y muestra: "El ISBN no puede estar vacio.".

int[] copiasDisponibles = { 2, 0, 3 };

try
{
    Console.WriteLine(copiasDisponibles[5]);
}
catch (IndexOutOfRangeException)
{
    Console.WriteLine("Esa posicion no existe en el array de copias disponibles.");
}
finally
{
    Console.WriteLine("Consulta finalizada.");
}

Al intentar acceder a copiasDisponibles[5] (el array solo tiene posiciones 0, 1 y 2), se lanza IndexOutOfRangeException; el catch muestra el mensaje de error y, a continuación, el finally muestra "Consulta finalizada." de todas formas.

Conclusión

En esta lección has aprendido qué es una excepción y cómo capturarla con try/catch, cómo garantizar que cierto código se ejecute siempre con finally, cómo lanzar tus propias excepciones con throw para señalar reglas de negocio incumplidas, los tipos de excepción más comunes de .NET, cómo capturar varios tipos ordenadamente, y por qué no conviene abusar de las excepciones para validaciones que se pueden anticipar. BiblioTechConsola ya puede reaccionar ante datos inválidos o situaciones imprevistas sin detenerse de golpe.

Con esto se cierra el Módulo 2: Estructuras de Control. Ya sabes tomar decisiones con condicionales, repetir acciones con bucles, clasificar valores con switch, y proteger tu programa frente a errores con excepciones —todo ello, hasta ahora, usando variables sueltas, arrays y funciones locales dentro de Main. Esa forma de trabajar empieza a mostrar sus límites según BiblioTechConsola crece: cada libro son varias variables sueltas (título, ISBN, disponibilidad...) que hay que mantener sincronizadas a mano, y cada socio, otras tantas. En el Módulo 3: Programación Orientada a Objetos aprenderás a modelar esos conceptos con clases, agrupando datos y comportamiento en un único lugar: el primer paso para que BiblioTechConsola deje de ser un conjunto de variables sueltas y empiece a parecerse a una aplicación real y bien organizada.

Curso de Programación en C#

Módulo 1: Introducción a C#

Módulo 2: Estructuras de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Conceptos Avanzados de C#

Módulo 5: Trabajando con Datos

Módulo 6: Temas Avanzados

Módulo 7: Construcción de Aplicaciones

Módulo 8: Mejores Prácticas y Patrones de Diseño

Módulo 9: Proyecto Final

© Copyright 2026. Todos los derechos reservados