C# Throw Keyword: Syntax, Rethrowing, and Filters
c# throw keyword: Learn how to use the C# throw keyword correctly: syntax, rethrowing exceptions, exception filters, custom exceptions, and performance considerations.
c# throw keyword requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
The throw keyword in C# is the mechanism for signaling that an exceptional condition has occurred. It can create a new exception, rethrow an existing one, or propagate an exception up the call stack. This article covers the syntax, common patterns, runtime behavior, and practical tradeoffs of using throw in real applications.
Basic Syntax of throw
The simplest form of throw is followed by an exception instance. The exception must derive from System.Exception or be a type that can be thrown by the runtime, such as System.Exception itself. Here is a minimal example:
public void ValidateAge(int age) { if (age < 0) { throw new ArgumentOutOfRangeException(nameof(age), "Age cannot be negative."); } }
When this method is called with a negative value, the ArgumentOutOfRangeException is constructed and thrown. Control transfers immediately to the nearest matching catch block in the call stack. Any code after the throw statement inside the same method is not executed.
The throw statement can also be used in a catch block to rethrow the exception that was caught. This preserves the original stack trace, which is critical for debugging. The distinction between throw; and throw ex; is a common source of bugs.
Rethrowing Exceptions and Preserving the Stack Trace
When you catch an exception, you may need to perform some logging or cleanup and then propagate the exception upward. The correct way to rethrow is to use throw; without an operand:
try { ProcessFile(path); } catch (IOException ex) { LogError(ex); throw; // preserves the original stack trace }
Using throw ex; instead resets the stack trace to the point of the throw statement, losing the original method that first threw the exception. This makes debugging significantly harder because the stack trace no longer shows where the exception originated. The following code demonstrates the problematic pattern:
try { ProcessFile(path); } catch (IOException ex) { LogError(ex); throw ex; // stack trace now points to this line }
The difference is subtle but has a large impact on production diagnostics. Always use throw; when you want to rethrow the same exception without adding new information.
If you need to add contextual data while preserving the original exception, you can throw a new exception and set the InnerException property:
try { ProcessFile(path); } catch (IOException ex) { throw new InvalidOperationException("Failed to process file.", ex); }
This creates a new exception with the original as its inner exception. The stack trace of the inner exception is preserved, and the new exception can carry higher-level context about the operation that failed.
Using throw with Exception Filters
C# 6.0 introduced exception filters, which allow a catch block to execute only when a condition is true. The when keyword is used with catch to filter exceptions. This is particularly useful when you want to handle the same exception type differently based on runtime properties.
try { SaveToDatabase(record); } catch (SqlException ex) when (ex.Number == 1205) { // Deadlock: retry logic RetryOperation(record); } catch (SqlException ex) when (ex.Number == 2627) { // Unique constraint violation HandleDuplicateRecord(ex); }
Exception filters are evaluated before the catch block is entered. If the filter returns false, the runtime continues searching for another matching catch block. This avoids the need to rethrow inside the catch block to test a condition. It also preserves the original stack trace because the exception is not rethrown.
Filters are evaluated on the exception object without unwinding the stack, which can be more efficient than catching and rethrowing. However, they should be used judiciously. A filter that performs expensive I/O or has side effects can cause unexpected behavior, because the filter may be evaluated multiple times if multiple catch blocks exist.
Throwing Custom Exceptions and Adding Context
When built-in exception types do not adequately describe the failure, you can define a custom exception class. The recommended pattern is to derive from Exception and provide the three standard constructors:
public class OrderProcessingException : Exception { public OrderProcessingException() { } public OrderProcessingException(string message) : base(message) { } public OrderProcessingException(string message, Exception inner) : base(message, inner) { } }
Then you can throw it with additional properties that carry domain-specific data:
public void ProcessOrder(Order order) { if (!order.IsValid) { throw new OrderProcessingException($"Order {order.Id} is invalid.") { OrderId = order.Id }; } }
Custom exceptions make error handling more expressive. Callers can catch the specific type and access the additional properties without parsing the message string. This improves maintainability because the contract between the throwing code and the catching code is explicit.
When throwing custom exceptions, keep the following in mind:
- Name the class with the
Exceptionsuffix. - Mark it
[Serializable]if you need to support binary serialization (though modern .NET favors other serialization approaches). - Provide the three standard constructors to remain consistent with framework conventions.
- Use the
InnerExceptionproperty to chain the original cause.
Performance and Runtime Considerations of throw
Throwing an exception is expensive compared to normal control flow. The runtime must capture the stack trace, allocate the exception object, and unwind the stack. In performance-sensitive paths, exceptions should be used only for exceptional conditions, not for regular control flow.
Consider a validation loop that checks user input. If invalid input is common, using throw for each invalid field can degrade throughput. Instead, return a result object that collects errors:
public ValidationResult Validate(UserInput input) { var errors = new List<string>(); if (string.IsNullOrWhiteSpace(input.Name)) errors.Add("Name is required."); if (input.Age < 0) errors.Add("Age cannot be negative."); return new ValidationResult(errors); }
This avoids exception overhead in the expected path. Exceptions remain appropriate for truly unexpected conditions, such as a missing configuration file or a network failure.
Another performance consideration is the stack trace capture. When you rethrow with throw;, the runtime appends the current stack frame to the existing trace. This is relatively cheap. However, creating a new exception with an inner exception duplicates the stack trace of the original, which adds allocation and formatting cost. If you are in a loop that throws and catches frequently, the overhead can become noticeable.
Exception filters have a subtle performance advantage: they are evaluated before the stack is unwound, so the runtime does not need to build the full stack trace for the filter to run. This can make filtering faster than catching and then conditionally rethrowing. However, the difference is usually negligible unless exceptions are thrown very frequently.
Common Mistakes and How to Avoid Them
One of the most common mistakes is using throw ex; instead of throw; in a catch block. As discussed, this loses the original stack trace. Always use throw; when you want to propagate the same exception.
Another mistake is throwing an exception from a finally block. If a finally block throws, it overrides any exception that is currently propagating, causing the original error to be lost. This is rarely intended. For example:
try { DoWork(); } finally { Cleanup(); // if this throws, it replaces the original exception }
If cleanup can fail, catch the exception inside finally and log it, or use a pattern that does not throw during cleanup.
A third mistake is using exceptions for control flow in a way that makes the code harder to read. For instance, throwing an exception to break out of a recursive algorithm is usually worse than using a return value. Exceptions should be reserved for failures, not for normal termination.
Finally, be careful when throwing exceptions from constructors. If a constructor throws, the object is not fully constructed, and the caller must handle the exception. This is acceptable when the object cannot be created with invalid state, but it means the caller must always wrap construction in a try-catch. Consider using a factory method that returns a result object for scenarios where construction failure is expected.
When to Avoid throw and Use Alternatives
The throw keyword is not always the best tool. For expected validation failures, returning a result object or using the Try pattern is often clearer and more efficient. The .NET base class library uses this pattern extensively, for example int.TryParse returns a bool and outputs the parsed value.
public bool TryParseDate(string input, out DateTime date) { return DateTime.TryParse(input, out date); }
This approach avoids exception overhead and makes the control flow explicit. Use throw when the failure is truly exceptional and the caller cannot reasonably recover without knowing the specific reason.
Another alternative is the Result type pattern, common in functional programming. A method returns a Result object that either contains a value or an error. This forces the caller to handle the error explicitly, reducing the chance of unhandled exceptions. However, it adds boilerplate and can obscure the main flow if overused.
The decision between throw and a result-based approach depends on the contract you want to establish. If the method's contract states that it will either succeed or throw, then throw is appropriate. If failure is a normal outcome that callers should handle, a result object is better. The key is to be consistent within a codebase and to document the behavior clearly.