Cuando algo sale mal en tiempo de ejecución (falta un archivo, un texto no es un número, una clave no está en un diccionario), .NET lanza una excepción: un objeto que describe el error. La excepción sube por la pila de llamadas hasta que un bloque catch la maneja. Si nada lo hace, el programa se detiene e imprime el error.
Un try catch básico
Envuelve en try el código que puede fallar, y maneja el fallo en catch:
Salida:
42 doubled is 84
'forty-two' is not a number
7 doubled is 14
Still running
Cuando int.Parse("forty-two") lanza la excepción, se salta el Console.WriteLine que va después dentro del bloque try, se ejecuta el bloque catch (FormatException) y el bucle continúa. Puedes omitir la variable (catch (FormatException)) cuando no necesitas el objeto excepción.
Este caso concreto tiene una herramienta mejor: int.TryParse(input, out int n) devuelve false en lugar de lanzar una excepción. Las excepciones son para situaciones que el código no espera; una entrada que a menudo es incorrecta es algo esperado, así que compruébala en lugar de capturar.
Cómo viaja una excepción
Una excepción lanzada en lo profundo de una cadena de llamadas deshace cada método hasta que encuentra un manejador que coincida. En cada uno de esos métodos, el código posterior al lanzamiento no se ejecuta.
Salida:
OrderTotal finished
5.00
Caught KeyNotFoundException in Main
PriceOf y OrderTotal no tienen catch, así que la excepción los atraviesa directamente hasta Main. Pon un manejador en el nivel que sabe qué hacer con el fallo, que muchas veces no es donde ocurrió.
Capturar excepciones concretas, en el orden correcto
Una cláusula catch maneja su tipo y todos los tipos derivados de él. Cuando hay varias cláusulas, gana la primera que coincide, así que ordénalas de la más específica a la más general:
Salida:
10 / 2 = 5
Cannot divide by zero
Both values must be whole numbers
Unexpected: OverflowException
La última llamada lanza OverflowException, que ninguna de las cláusulas específicas maneja, así que la maneja el catch (Exception e) general. Poner catch (Exception) primero haría inalcanzables las cláusulas que van detrás, y el compilador lo rechaza con el error CS0160.
Filtros de excepción con when
C# 6 añadió when: una condición que decide si una cláusula catch se aplica. Si es false, el runtime sigue buscando otro manejador como si la cláusula no existiera.
Salida:
Not found: show an empty page
Server error 503: retry later
Unhandled by Call: HTTP 401
Los filtros te permiten ramificar según los datos de la excepción sin capturarla y volver a lanzarla. También son la forma limpia de manejar igual dos tipos sin relación, como hace la tercera cláusula. Un filtro se ejecuta antes de deshacer la pila, así que un depurador o un volcado de memoria siguen mostrando el estado original cuando ningún filtro coincide.
finally: código que siempre se ejecuta
Un bloque finally se ejecuta cuando el control sale del try, tanto si terminó como si volvió antes o lanzó una excepción. Es donde va la limpieza. (Una salvedad: si nada en ningún sitio captura la excepción, el proceso puede terminar sin ejecutarlo.)
Salida:
Open connection
Close connection
finished
Open connection
Close connection
returned early
Open connection
Close connection
handled error
En todos los casos, "Close connection" se imprime antes de que el resultado del método llegue a Main: el finally se ejecuta después de calcular el valor de return pero antes de que el método vuelva de verdad. Un try puede tener un finally sin ningún catch, lo que limpia y deja que la excepción siga hacia quien llama.
Para los objetos que implementan IDisposable (archivos, streams, conexiones), la sentencia using escribe este try/finally por ti.
Volver a lanzar: throw; frente a throw e;
A veces un bloque catch registra o anota algo y después deja que la excepción siga. Cómo la vuelves a lanzar decide si la traza de la pila sobrevive:
Salida:
throw; trace mentions LoadConfig: True
throw e; trace mentions LoadConfig: False
throw e; trata la excepción como si se acabara de lanzar desde el bloque catch, así que los marcos que hay por debajo, incluido el método donde ocurrió el error, desaparecen de la traza. Vuelve a lanzar siempre con un throw; a secas. (El atributo NoInlining solo está ahí porque el JIT puede fusionar un método tan pequeño con su llamador, lo que lo ocultaría de las dos trazas.)
Para añadir contexto, envuelve la excepción en una nueva y pasa la original como excepción interna: throw new ConfigException("Could not start the app", e);. La excepción interna conserva su propia traza de pila, y los loggers imprimen la cadena. Cómo escribir tus propios tipos de excepción se explica en lanzar excepciones.
El objeto Exception
Toda excepción deriva de System.Exception. Los miembros que más usarás:
| Miembro | Qué contiene |
|---|---|
Message | Una descripción legible |
GetType().Name | El tipo de la excepción, como FormatException |
StackTrace | La cadena de llamadas a métodos en el punto del lanzamiento |
InnerException | La excepción que causó esta, o null |
ToString() | Tipo, mensaje, excepciones internas y traza de pila juntos |
Registra e.ToString() en lugar de e.Message cuando quieras diagnosticar un fallo más adelante: el mensaje solo rara vez dice dónde estaba el problema. No muestres e.ToString() a los usuarios finales.
Tipos de excepción comunes
| Excepción | Causa típica |
|---|---|
NullReferenceException | Llamar a un miembro sobre una referencia null |
ArgumentNullException | Se pasó null a un método que necesita un valor |
ArgumentOutOfRangeException | Un argumento o índice de lista está fuera del rango permitido |
IndexOutOfRangeException | Un índice de array está fuera de sus límites |
FormatException | int.Parse, DateTime.Parse y similares sobre un texto con el formato equivocado |
InvalidCastException | Un cast explícito a un tipo que el objeto no es |
InvalidOperationException | El objeto está en un estado incorrecto para la llamada (secuencia vacía, colección modificada) |
KeyNotFoundException | Leer con el indexador una clave de diccionario que falta |
DivideByZeroException | División entera o decimal entre cero |
OverflowException | Un número parseado, una conversión checked o un resultado aritmético checked no cabe en el tipo |
FileNotFoundException, IOException | Problemas del sistema de archivos |
NullReferenceException, IndexOutOfRangeException e InvalidCastException casi siempre indican un bug. Arregla el código en lugar de capturarlas.
No tragarse las excepciones
Un catch vacío oculta todos los errores, incluidos los que no previste:
try
{
SaveOrder(order);
}
catch (Exception)
{
// nothing: the order silently was not saved
}
El programa sigue funcionando como si el guardado hubiera ido bien, y la causa real se pierde. Pautas que mantienen honesto el manejo de excepciones:
- Captura solo lo que puedes manejar, en el nivel que puede manejarlo.
- Si capturas para registrar, vuelve a lanzar con
throw;salvo que el programa de verdad pueda continuar. - Captura
Exceptionsolo en el borde exterior:Main, un manejador de peticiones, un bucle de trabajo. - Prefiere
TryParse,TryGetValuey las comprobaciones denulla las excepciones en los casos esperados. Lanzar es lento comparado con una comprobación, mientras que un bloquetryque no lanza nada no cuesta casi nada.
Errores comunes
catch (Exception)primero. Las cláusulas posteriores se vuelven inalcanzables (CS0160).throw e;para volver a lanzar. Borra la traza de pila original; usathrow;.- Bloques catch vacíos. Los errores desaparecen; como mínimo registra y vuelve a lanzar.
- Usar excepciones para el control del flujo. Valida la entrada con
TryParseen lugar de capturarFormatException. - Mostrar a los usuarios el
e.Messagede una excepción del framework. El texto cambia entre versiones de .NET y está escrito para desarrolladores.
Preguntas frecuentes
¿Cómo funciona try catch en C#?
El código que puede fallar va en el bloque try. Si una sentencia de ahí lanza una excepción, el resto del bloque se salta y el runtime busca una cláusula catch cuyo tipo coincida con la excepción, primero en el método actual y después en cada llamador. Se ejecuta el primer catch que coincide, y la ejecución continúa después de toda la sentencia try.
¿Se ejecuta siempre finally en C#?
Casi siempre: después de que el bloque try termine con normalidad, después de que un catch maneje una excepción, después de un return o un break dentro del bloque, y cuando una excepción pasa por él camino de un catch más arriba en la pila de llamadas. No se ejecuta cuando el proceso termina antes: un proceso matado, Environment.FailFast, una StackOverflowException y, en .NET Core y posteriores, una excepción que nada captura, que termina el proceso antes de que se ejecuten los bloques finally.
¿Cómo capturo varias excepciones en C#?
Escribe varias cláusulas catch, primero el tipo más específico: catch (FileNotFoundException) antes de catch (IOException) y este antes de catch (Exception). El compilador rechaza una cláusula que nunca puede alcanzarse porque una anterior ya captura su tipo. Para manejar igual dos tipos sin relación, usa un filtro: catch (Exception e) when (e is FormatException || e is OverflowException).
¿Qué diferencia hay entre throw y throw ex en C#?
Dentro de un catch, throw; vuelve a lanzar la excepción actual con su traza de pila original. throw ex; lanza el mismo objeto pero reinicia la traza de pila en la línea actual, así que el método donde realmente ocurrió el error desaparece de la traza. Usa throw;, o envuélvela: throw new MyException("context", ex);.
¿Qué es un filtro de excepciones en C#?
Una cláusula when después de un catch (C# 6 y posteriores): catch (HttpRequestException e) when (e.Message.Contains("404")). El bloque catch solo se ejecuta si la condición es true; si no, la excepción sigue buscando otro manejador como si la cláusula no existiera, y su pila no se deshace.
¿Debo capturar Exception en C#?
Solo en los bordes de un programa: el principio de Main, un manejador de peticiones o un bucle en segundo plano, donde la tarea es registrar el error y seguir, o salir limpiamente. En lo profundo del código, captura los tipos concretos que de verdad puedes manejar y deja que todo lo demás se propague.