NashTech Blog

Table of Contents

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 the try block.
  • catch: If an exception occurs, control is transferred to the catch block, where we can handle the exception.
  • finally: This block is optional. It executes after the try and catch blocks, 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 Exception unless necessary: Catching Exception is a broad approach. It’s often better to catch specific exceptions.
  • Don’t suppress exceptions: Avoid using empty catch blocks. 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.

Picture of Ajay Jajoo

Ajay Jajoo

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top