Robust Exception Handling in C# with try-catch-finally Constructs

C# provides a structured mechanism for handling runtime errors through the try, catch, and finally keywords. This approach enables developers to enticipate, intercept, and respond to exceptional conditions without abrupt program termination.

The .NET runtime defines a rich hierarchy of exception types rooted in the base class System.Exception. While built-in exceptions like DivideByZeroException or NullReferenceException cover common failure modes, custom exceptions can be introduced by inheriting from Exception or one of its standard derivatives.

Basic Syntax and Flow

A minimal exception-handling construct requires only a try block, optionally followed by one or more catch clauses and/or a single finally clause:

try
{
    // Code that may throw
}
catch (InvalidOperationException ex)
{
    // Handle specific exception
}
catch (Exception ex)
{
    // Fallback handler
}
finally
{
    // Always executes — cleanup logic here
}

Execution proceeds as follows:

  • If no exception occurs, catch blocks are skipped; finally runs before control exits the block.
  • If an exception is thrown, the runtime searches for the first matching catch (by type compatibility), executes it, then transfers control to finally.
  • The finally block always executes — even if return, break, or goto appears earlier in try or catch. However, exiting finally via those statements is prohibited.

Practical Example: Division Safeguard

Consider unsafe integer division:

int divisor = 0;
int result = 100 / divisor; // Throws DivideByZeroException

With protection:

int divisor = 0;
int result = 0;

try
{
    result = 100 / divisor;
    Console.WriteLine("This won't print");
}
catch (DivideByZeroException)
{
    Console.WriteLine("Division by zero prevented");
}
finally
{
    Console.WriteLine("Cleanup completed");
}

Console.WriteLine($"Final value: {result}");

Output:

Division by zero prevented
Cleanup completed
Final value: 0

Multiple Catch Handlers

When multiple exception types may arise, order mattters: more specific handlers must precede broader ones.

try
{
    ProcessInput(userInput);
}
catch (ArgumentNullException argEx)
{
    LogError("Null input received", argEx);
}
catch (FormatException fmtEx)
{
    LogError("Invalid format detected", fmtEx);
}
catch (Exception genericEx)
{
    LogError("Unexpected error occurred", genericEx);
}

Omitting the exception type in catch creates a universal handler:

catch
{
    // Catches any exception — use sparingly
}

Throwing and Re-throwing Exceptions

To signal failure programmatically, use throw:

if (value < 0)
    throw new ArgumentException("Value must be non-negative");

Within a catch, re-throwing preserves original stack trace:

catch (IOException ioEx)
{
    LogWarning("I/O issue encountered");
    throw; // Not 'throw ioEx'
}

Custom Exception Implementation

Define domain-specific exceptions for clarity:

public class InsufficientFundsException : Exception
{
    public decimal AvailableBalance { get; }
    public decimal AttemptedWithdrawal { get; }

    public InsufficientFundsException(
        decimal balance, 
        decimal withdrawal) 
        : base($"Cannot withdraw {withdrawal:C} — balance is {balance:C}")
    {
        AvailableBalance = balance;
        AttemptedWithdrawal = withdrawal;
    }
}

Usage:

if (account.Balance < withdrawalAmount)
    throw new InsufficientFundsException(account.Balance, withdrawalAmount);

Return Behavior with Finally

A return inside try does not immediately exit — finally executes first, though the return value remains unchanged:

static int GetValue()
{
    int val = 42;
    try
    {
        Console.WriteLine("In try");
        return val;
    }
    finally
    {
        val *= 2;
        Console.WriteLine("In finally");
    }
}

Output:

In try
In finally
42

The returned value is still 42, because the return expression was evaluated before finally ran.

Best Practicse

  • Prefer built-in exceptions over custom ones unless semantics significantly differ.
  • Avoid empty catch blocks — they suppress failures silently.
  • Use finally for deterministic cleanup (e.g., closing streams, releasing locks).
  • Never throw SystemException subclasses — they indicate runtime-level faults.
  • When wrapping exceptions, use throw new CustomException("message", innerException) to preserve diagnostic context.

Tags: csharp exception-handling Try-Catch-Finally error-handling

Posted on Sat, 07 Nov 2026 16:43:23 +0000 by amosse