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
- ¿Qué es una excepción?
try/catch: capturar erroresfinally: código que siempre se ejecuta- Lanzar excepciones propias con
throw - Tipos de excepción comunes
- Capturar múltiples tipos de excepción
- Buenas prácticas con excepciones
- Aplicación completa: prestar un libro de forma segura
- ¿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.
try/catch: capturar errores
try/catch: capturar erroresEl 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}");
}
finally: código que siempre se ejecuta
finally: código que siempre se ejecutaEl 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.
- Lanzar excepciones propias con
throw
throwAdemá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.
- 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}");
}
- 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.
- 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.
- 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
Exceptiongené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
ifo conTryParse, hazlo; no dejes que el programa falle "a propósito" para luego capturarlo. - Bloques
catchvací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
finallyo las construcciones que ya se encargan de ello automáticamente (lo verás conusingen 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
-
Escribe un bloque
try/catchque intente convertir la cadena"cien"aintconint.Parse, capture la excepciónFormatExceptiony muestre por consola un mensaje indicando que la entrada no es un número válido. -
Crea una función local
void ValidarISBN(string isbn)que lance unaArgumentExceptioncon el mensaje"El ISBN no puede estar vacio."si el parámetroisbnes una cadena vacía (""), y que en caso contrario muestre por consola"ISBN valido.". Llama a la función con""dentro de untry/catchque capture la excepción y muestre su mensaje. -
Dado el array
int[] copiasDisponibles = { 2, 0, 3 };, escribe untry/catch/finallyque intente acceder a la posición5del array (que no existe), capture la excepciónIndexOutOfRangeExceptionmostrando un mensaje adecuado, y en elfinallymuestre"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#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
