C# Exception Handling (try, catch, finally)
Learn how to handle exceptions in C# using try, catch, and finally blocks to manage errors safely with clear examples.
In C#, error handling is used to catch and manage unexpected situations during program execution.
This prevents the program from crashing abruptly and allows you to show appropriate messages or take alternative actions.
Error handling uses the try, catch, and finally blocks together.
try-catch Usage
The try block contains code that may throw an exception.
If an error occurs, the catch block handles it.
try
{
int number = int.Parse("abc"); // Invalid conversion
Console.WriteLine("Number: " + number);
}
catch (FormatException ex)
{
Console.WriteLine("Error: Invalid number format.");
}
// Output:
Error: Invalid number format.
Multiple catch Blocks
You can use multiple catch blocks for different error types.
This allows handling each exception type differently.
try
{
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[5]); // Index out of range
}
catch (IndexOutOfRangeException)
{
Console.WriteLine("Error: Array index out of range.");
}
catch (Exception ex)
{
Console.WriteLine("Unexpected error: " + ex.Message);
}
Advanced Error Handling: Exception Filters (when)
In C#, exception filters allow you to apply a condition to a
catch block using the when keyword.
This makes it possible to handle an exception only if a specific condition is met.
The key advantage of exception filters is that the condition is evaluated
before entering the catch block.
If the condition evaluates to false, the runtime skips that block
and continues searching for another matching catch clause.
This results in:
- Cleaner and more readable error handling
- More precise control over exception logic
- Reduced need for nested
ifstatements inside catch blocks
Example: Handling HTTP Errors by Status Code
In real-world applications, HTTP requests often return different status codes. Using exception filters, we can handle specific status codes differently while keeping the code structured and readable.
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using var client = new HttpClient();
try
{
HttpResponseMessage response =
await client.GetAsync("https://api.example.com/users/1");
response.EnsureSuccessStatusCode();
string content = await response.Content.ReadAsStringAsync();
Console.WriteLine(content);
}
catch (HttpRequestException ex)
when (ex.StatusCode == HttpStatusCode.NotFound)
{
Console.WriteLine("Resource not found (404).");
}
catch (HttpRequestException ex)
when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
Console.WriteLine("Unauthorized access (401). Check your credentials.");
}
catch (HttpRequestException ex)
{
Console.WriteLine("A general HTTP error occurred.");
Console.WriteLine($"Status Code: {ex.StatusCode}");
}
}
}
In this example:
- If the API returns
404 Not Found, a specific message is shown. - If the API returns
401 Unauthorized, it is handled separately. - Other HTTP-related errors fall back to the general handler.
- The
whencondition is evaluated before entering thecatchblock. - If the condition is
false, the runtime skips thatcatchclause. - Exception filters were introduced in C# 6 and are widely used in modern backend development.
Rethrowing Exceptions: throw; vs throw ex;
An exception caught inside a catch block may need to be rethrown.
However, throw; and throw ex; do not behave the same way.
The main difference lies in whether the stack trace is preserved. The stack trace shows the chain of method calls that led to the error and the exact line where it originally occurred.
-
throw;Rethrows the exception while preserving its original source (the line number and the method where the error occurred). During debugging, you can see exactly where the exception actually started. This is the correct and recommended approach. -
throw ex;Resets the stack trace at that line and makes it appear as if the error occurred inside thecatchblock. In this case, the real source of the exception (for example, a deeper method) is lost, making debugging more difficult.
Example
using System;
class Program
{
static void Main()
{
try
{
MethodA();
}
catch (Exception ex)
{
Console.WriteLine("Error logged.");
// Correct usage:
throw;
// Incorrect usage:
// throw ex;
}
}
static void MethodA()
{
MethodB();
}
static void MethodB()
{
throw new InvalidOperationException("An error occurred.");
}
}
In this example, if throw; is used, the exception will appear
in the stack trace as originating from MethodB.
If throw ex; is used instead, the exception will appear
as if it originated from inside the catch block,
and the original call chain will be lost.
- If an exception needs to be rethrown,
throw;should always be preferred. throw ex;makes debugging harder because it breaks the original stack trace.- The
throwkeyword can also be used to throw custom exception classes.
Custom Exceptions
In real-world applications, not every error should be represented by
general exception types such as Exception or other built-in exceptions.
To clearly express business rule violations and domain-specific problems,
you can define custom exception classes.
This approach:
- Helps separate error types more clearly
- Allows more specific handling in catch blocks
- Provides additional contextual information for logging and diagnostics
- Improves code readability and maintainability
Example
// 1. Custom exception definitions
public class ProjectNotFoundException : Exception
{
public ProjectNotFoundException(string message) : base(message) { }
}
public class ProjectIsCanceledException : Exception
{
public int ProjectId { get; }
public string ProjectName { get; }
public ProjectIsCanceledException(int projectId, string projectName)
: base($"Project '{projectName}' (Id: {projectId}) is canceled and cannot be processed.")
{
ProjectId = projectId;
ProjectName = projectName;
}
}
// 2. Usage
public void ProcessProject(Project project)
{
if (project == null)
{
throw new ProjectNotFoundException("Project is not registered in the system.");
}
if (project.Status == ProjectStatus.Canceled)
{
throw new ProjectIsCanceledException(project.Id, project.Name);
}
// ...
}
In this example, if the project does not exist in the system,
a ProjectNotFoundException is thrown.
If the project has been canceled,
ProjectIsCanceledException is thrown.
This exception carries not only an error message,
but also the ProjectId and ProjectName values,
making logging and error analysis more meaningful.
- Custom exception classes usually represent domain or business rule violations.
- Naming convention typically follows the
SomethingExceptionpattern. - Additional properties can be added to carry more contextual information.
finally Block
The finally block always runs, regardless of whether an error occurred or not.
It is often used for cleanup operations like closing files or terminating database connections.
try
{
Console.WriteLine("Opening file...");
throw new Exception("File not found!");
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex.Message);
}
finally
{
Console.WriteLine("Closing file...");
}
// Output:
Opening file...
Error: File not found!
Closing file...
Throwing Custom Exceptions
You can define your own error conditions and throw exceptions using throw.
static void Divide(int a, int b)
{
if (b == 0)
throw new DivideByZeroException("Division by zero error!");
Console.WriteLine("Result: " + (a / b));
}
static void Main()
{
try
{
Divide(10, 0);
}
catch (DivideByZeroException ex)
{
Console.WriteLine("Error: " + ex.Message);
}
}
// Output:
Error: Division by zero error!
TL;DR
try: Block containing code that may throw exceptions.catch: Handles exceptions when they occur.finally: Always executes, typically used for cleanup.- Multiple
catchblocks allow handling different exception types separately. - Exception filters (
when) allow conditional exception handling. throw;preserves the original stack trace when rethrowing an exception.throw ex;resets the stack trace and should generally be avoided.- Custom exception classes help represent domain-specific or business rule errors.
Related Articles
C# Conditional Statements (if, else, switch)
Decision structures in C#: learn how to use if, else if, else and switch to perform different actions based on conditions.
C# Loops (for, foreach, while, do-while)
Learn how to use for, foreach, while, and do-while loops in C#. Discover practical examples for handling repeated operations in C# applications.
Interop in C# (Working with C/C++ Libraries)
Learn how to use Interop in C# to work with C/C++ libraries, including P/Invoke, unmanaged code, and data marshaling.
Methods and Parameter Usage in C#
Learn how to define methods and use parameters in C#, including value and reference parameters, optional parameters, and examples.
SOLID Principles with C#
Applying SOLID principles in C# with examples: building flexible, maintainable, and testable code.