Loading...

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:

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:

  • The when condition is evaluated before entering the catch block.
  • If the condition is false, the runtime skips that catch clause.
  • 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.

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 throw keyword 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:

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 SomethingException pattern.
  • 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 catch blocks 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