Exception handling is a critical aspect of programming in C#. It allows developers to anticipate, manage, and recover from runtime errors, ensuring that applications remain robust and user-friendly even when things go wrong. In this blog, we’ll dive deep into C# exception handling, providing examples, best practices, and important concepts.
1. What are Exceptions in C#?
In C#, an exception is an event that disrupts the normal flow of a program. It typically represents a runtime error such as division by zero, file not found, or null reference. Exceptions are objects derived from the System.Exception class, and they can be caught and handled using structured exception handling (SEH).
2. The Try-Catch-Finally Block
C# provides a structured way to handle exceptions using the try-catch-finally block:
try: Code that might throw an exception is placed inside thetryblock.catch: If an exception occurs, control is transferred to thecatchblock, where we can handle the exception.finally: This block is optional. It executes after thetryandcatchblocks, regardless of whether an exception occurred, making it ideal for cleanup code like closing file handles or database connections.
try
{
int result = 10 / 0; // Will throw DivideByZeroException
}
catch (DivideByZeroException ex)
{
Console.WriteLine("Error: Division by zero!");
}
finally
{
Console.WriteLine("Cleanup code executed.");
}
3. Catch Multiple Exceptions
We can catch different types of exceptions in separate catch blocks. The order of catch blocks is important because they are evaluated from top to bottom.
try
{
string text = null;
int length = text.Length; // Throws NullReferenceException
}
catch (NullReferenceException ex)
{
Console.WriteLine("Error: Null reference encountered!");
}
catch (Exception ex)
{
Console.WriteLine("General exception: " + ex.Message);
}
4. Using Exception Filters
Exception filters allow us to handle specific exceptions under certain conditions. This is useful if we only want to catch an exception when certain criteria meet.
try
{
int result = int.Parse("abc"); // Will throw FormatException
}
catch (FormatException ex) when (ex.Message.Contains("Input"))
{
Console.WriteLine("Invalid format exception with specific message.");
}
5. Custom Exception Handling
C# allows us to create custom exceptions by inheriting from the System.Exception class. This is helpful when we need to define specific error conditions in our application.
public class InvalidAgeException : Exception
{
public InvalidAgeException(string message) : base(message) { }
}
public class Program
{
public static void Main()
{
try
{
throw new InvalidAgeException("Age cannot be negative.");
}
catch (InvalidAgeException ex)
{
Console.WriteLine("Custom Exception: " + ex.Message);
}
}
}
6. Throwing Exceptions
We can throw exceptions manually using the throw keyword. This is useful when a condition in our code does not meet, and we need to signal an error.
public void ProcessAge(int age)
{
if (age < 0)
{
throw new ArgumentException("Age cannot be negative.");
}
// Continue processing...
}
7. Exception Propagation
In C#, exceptions propagate up the call stack. This means that if the current method doesn’t catch an exception, the exception moves up to the caller, and continues until the caller catches it or the program ends. If not caught, it will cause the application to crash.
public void MethodA()
{
MethodB();
}
public void MethodB()
{
throw new InvalidOperationException("Something went wrong.");
}
8. Best Practices for Exception Handling
- Use specific exceptions: Catch specific exceptions before general ones to ensure precise handling.
- Avoid catching
Exceptionunless necessary: CatchingExceptionis a broad approach. It’s often better to catch specific exceptions. - Don’t suppress exceptions: Avoid using empty
catchblocks. It’s better to log the error or handle it in a meaningful way. - Use custom exceptions for clarity: When needed, define our own exception classes to make error handling more descriptive.
- Don’t overuse exceptions for control flow: We should use exceptions for exceptional cases, not for regular program logic.
9. Handling Asynchronous Exceptions
In C#, async methods may throw exceptions that the program handles asynchronously. We can catch exceptions in async methods using a try-catch block, and we can also use Task.WhenAll() to handle multiple asynchronous operations.
public async Task ExampleAsync()
{
try
{
await Task.Run(() => throw new InvalidOperationException("Async error"));
}
catch (InvalidOperationException ex)
{
Console.WriteLine("Caught exception in async task: " + ex.Message);
}
}
10. Logging and Rethrowing Exceptions
It’s a good practice to log exceptions before rethrowing them, especially in production environments. This helps with debugging without hiding the issue.
try
{
// Code that may throw an exception
}
catch (Exception ex)
{
Console.WriteLine($"Logging: {ex.Message}"); // Log exception
throw; // Rethrow the exception to preserve the stack trace
}
11. Handling Unhandled Exceptions
We can handle uncaught exceptions globally in a C# application using the AppDomain.CurrentDomain.UnhandledException event or the TaskScheduler.UnobservedTaskException event for tasks.
using System;
class Program
{
static void Main()
{
// Subscribe to the unhandled exception event
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
// Example of an unhandled exception
throw new InvalidOperationException("Something went wrong!");
}
static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Exception ex = (Exception)e.ExceptionObject;
Console.WriteLine($"Unhandled exception: {ex.Message}");
// Log the exception (optional)
// Optionally perform clean-up or notify users
// Exit the application gracefully (optional)
Environment.Exit(1);
}
}
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// Subscribe to the unobserved task exception event
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
// Create a task that throws an exception and is not awaited
Task.Run(() =>
{
throw new InvalidOperationException("Task exception not caught!");
});
// Give the task some time to throw the exception before the program exits
Task.Delay(1000).Wait();
}
static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
Exception ex = e.Exception;
Console.WriteLine($"Unobserved task exception: {ex.Message}");
// Mark the exception as handled to prevent the application from crashing
e.SetObserved();
}
}
Conclusion
Effective exception handling is a crucial part of writing stable and maintainable C# applications. By properly managing errors, developers can ensure that their programs remain user-friendly and resilient even in the face of unexpected conditions. Always keep in mind the best practices for exception handling to improve the quality and readability of our code.