Capturar excepciones es la mitad del manejo de errores; la otra mitad es lanzar la correcta. Una excepción bien elegida le dice a quien llama exactamente qué salió mal y si fue un error suyo o del estado del programa. Esta página trata el lado del lanzamiento; try catch trata el manejo.
La sentencia throw y las cláusulas de guarda
throw recibe un objeto excepción. La ejecución del método se detiene ahí, y la excepción busca un manejador subiendo por la pila de llamadas. El uso más común es una cláusula de guarda: comprobaciones al principio de un método que rechazan una entrada no válida antes de hacer ningún trabajo.
Salida:
ArgumentOutOfRangeException for parameter 'amount'
InvalidOperationException: The account is frozen.
Balance: 100
nameof(amount) produce el string "amount" y sigue siendo correcto si se renombra el parámetro. Las excepciones de argumento lo guardan en ParamName, que las herramientas y los logs usan para señalar el argumento incorrecto.
Las cláusulas de guarda mantienen sencillo el resto del método: pasadas las comprobaciones, el código puede suponer que la entrada es válida. Además fallan en el punto del error en lugar de dejar que un valor incorrecto siga su camino y provoque una confusa NullReferenceException tres métodos más allá.
Qué tipo de excepción lanzar
Reutiliza un tipo incorporado cuando describe la situación; quien llama ya sabe cómo manejarlo.
| Situación | Lanza |
|---|---|
Un argumento obligatorio es null | ArgumentNullException |
| Un argumento está fuera del rango permitido (cantidad negativa, índice más allá del final) | ArgumentOutOfRangeException |
| Un argumento no es válido por otro motivo (nombre vacío, ID mal formado) | ArgumentException |
| La llamada no es válida en el estado actual del objeto | InvalidOperationException |
Este tipo nunca admite la operación (el Add de una colección de solo lectura) | NotSupportedException |
| El método todavía no se ha escrito | NotImplementedException |
Se usó un objeto después de Dispose | ObjectDisposedException |
| Una operación con tiempo límite se quedó sin tiempo | TimeoutException |
La frontera entre las tres primeras filas e InvalidOperationException es quién tiene que cambiar algo. Una excepción de argumento dice "llama a esto de otra forma". InvalidOperationException dice "la llamada estaba bien, pero no ahora".
No lances Exception, SystemException ni ApplicationException directamente: quien llama no puede capturarlas sin capturar también todo lo demás. Tampoco lances tú mismo NullReferenceException, IndexOutOfRangeException ni StackOverflowException; el runtime las reserva para bugs reales.
Expresiones throw
Antes de C# 7, throw era solo una sentencia. Desde C# 7 también puede aparecer como expresión en tres sitios, lo que convierte las comprobaciones habituales en una sola línea:
Salida:
Ana <ana@example.com>
Null: name
ArgumentException: email
Ten en cuenta que ArgumentNullException deriva de ArgumentException, así que un catch (ArgumentException) puesto primero capturaría también el caso null. El orden de las cláusulas catch importa por la misma razón cuando manejas las dos.
ThrowIfNull y compañía (.NET 6 y posteriores)
El .NET moderno añade métodos auxiliares static que escriben por ti la comprobación y el lanzamiento, con el nombre del parámetro capturado automáticamente:
public void Ship(Order order, int quantity, string address)
{
ArgumentNullException.ThrowIfNull(order); // .NET 6
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity); // .NET 8
ArgumentException.ThrowIfNullOrWhiteSpace(address); // .NET 8
ObjectDisposedException.ThrowIf(disposed, this); // .NET 7
// ...
}
Se comportan igual que el if y el throw escritos a mano, y dejan cada cláusula de guarda en una línea. En destinos más antiguos, escribe la forma con if mostrada antes.
Escribir una clase de excepción propia
Crea tu propio tipo de excepción cuando quien llama necesite capturar este fallo concreto por separado, o cuando el manejador necesite datos que un string de mensaje no transmite bien.
Salida:
Cannot withdraw 25 from a balance of 15.
Short by 10
Las convenciones:
- El nombre termina en
Exception. - Deriva de
Exception(o de un tipo incorporado más específico cuando es un caso particular de él, comoInvalidOperationException). - Tiene los tres constructores habituales: sin argumentos, con mensaje y con mensaje más excepción interna. Añade tus propios constructores encima.
- Los datos extra van en propiedades de solo lectura, asignadas en el constructor. Así un manejador puede actuar sobre
e.Requesteden lugar de analizar el mensaje.
Envolver con una excepción interna
Cuando un fallo de bajo nivel debe aparecer como uno de nivel más alto, envuélvelo. El original se conserva como InnerException, así que no se pierde información:
Salida:
Setting 'port' must be a number, got '80a'.
Caused by: FormatException
Quien llama trabaja ahora en términos de configuración, que es lo que entiende, y registrar e.ToString() imprime toda la cadena, incluida la FormatException y su traza de pila. Envuelve solo cuando añades significado; envolver todas las excepciones en una MyAppException genérica solo obliga a los manejadores a escarbar en InnerException.
Lanzar frente a devolver un resultado
Las excepciones son para fallos que quien llama no espera en el funcionamiento normal. Para resultados rutinarios, como una búsqueda que a menudo no encuentra nada o una entrada del usuario que a menudo no es válida, la convención de .NET es el patrón Try: devolver bool y entregar el valor mediante un parámetro out.
public bool TryWithdraw(decimal amount, out string error)
{
if (amount > Balance) { error = "Insufficient funds."; return false; }
Balance -= amount;
error = null;
return true;
}
Muchos tipos ofrecen las dos cosas: int.Parse lanza una excepción, int.TryParse devuelve false; dict[key] lanza una excepción, dict.TryGetValue devuelve false. Lanzar una excepción cuesta mucho más que devolver un valor, así que no debería estar en un camino que se ejecuta miles de veces por segundo. Consulta ref y out para los parámetros out.
Escribir buenos mensajes
Un mensaje de excepción lo lee un desarrollador que mira un log. Haz que diga qué estaba mal y, cuando sea seguro, el valor causante: "Quantity must be between 1 and 99, got 0." es mejor que "Invalid input." Escribe frases completas, y deja fuera de los mensajes los secretos como contraseñas y tokens, porque acaban en los archivos de log.
Errores comunes
- Lanzar
Exceptiondirectamente. Quien llama no puede capturarla de forma selectiva; usa un tipo específico. - Pasar el mensaje donde va el nombre del parámetro.
new ArgumentNullException("name")recibe el nombre del parámetro; el mensaje va en segundo lugar. - Nombres de parámetro escritos a mano. Usa
nameof(param)para que sigan siendo correctos tras un renombrado. - Excepciones propias sin significado extra. Si encaja un tipo incorporado, úsalo.
- Perder el error original al envolver. Pásalo siempre como excepción interna.
Preguntas frecuentes
¿Cómo lanzo una excepción en C#?
Crea un objeto excepción y lánzalo: throw new ArgumentException("Amount must be positive", nameof(amount));. La ejecución se detiene en esa línea y la excepción sube por la pila de llamadas hasta el catch más cercano que coincida. Elige el tipo incorporado más específico que describa el problema, o un tipo propio cuando quien llama necesite manejar este caso por separado.
¿Cómo creo una excepción personalizada en C#?
Deriva de Exception una clase cuyo nombre termine en Exception y dale los constructores habituales: uno sin argumentos, uno que recibe un mensaje y uno que recibe un mensaje y una excepción interna, cada uno llamando al constructor base(...) correspondiente. Añade propiedades de solo lectura para cualquier dato que necesite un manejador, como el ID de un pedido o un saldo.
¿Cuándo debo lanzar ArgumentException y cuándo InvalidOperationException?
Lanza una ArgumentException (o ArgumentNullException / ArgumentOutOfRangeException) cuando quien llama pasó un valor incorrecto: la solución es llamar al método de otra forma. Lanza InvalidOperationException cuando los argumentos están bien pero el objeto está en un estado incorrecto para la llamada, como leer de una conexión cerrada o retirar dinero de una cuenta congelada.
¿Qué es una expresión throw en C#?
Desde C# 7, throw puede usarse como expresión en tres sitios: después de ??, como cualquiera de las ramas de ?:, y como cuerpo de un miembro con cuerpo de expresión o de una lambda. Por ejemplo, _name = name ?? throw new ArgumentNullException(nameof(name)); asigna o lanza en una sola línea.
¿Qué hace ArgumentNullException.ThrowIfNull?
Es un método auxiliar static añadido en .NET 6: ArgumentNullException.ThrowIfNull(customer); lanza ArgumentNullException con el nombre del parámetro rellenado automáticamente cuando customer es null, y no hace nada en caso contrario. Versiones posteriores añadieron métodos auxiliares parecidos, como ArgumentException.ThrowIfNullOrEmpty (.NET 7) y ArgumentOutOfRangeException.ThrowIfNegative (.NET 8).