Paramclasses Academy - Blog

Tutorials Details

←
Post 20 of 30
→

What is Exception Handling in C# ? Explain: Try, Catch, Finally and Custom Exceptions

What is Exception Handling in C# ? Explain: Try, Catch, Finally and Custom Exceptions

October 26, 2026

Introduction

While developing applications in C#, errors can occur for many reasons. A user may enter invalid input, a file may not exist, a database connection may fail, or a program may attempt to divide a number by zero.

If these situations are not handled properly, an application may terminate unexpectedly. Exception handling helps developers detect runtime errors, respond to them appropriately, and maintain a more reliable application.

In this tutorial, you will learn how exception handling works in C#, how to use try, catch, and finally, how to throw exceptions, and how to create custom exceptions with practical examples.

What You Will Learn

  • What exceptions are in C#

  • The difference between compile-time errors and runtime exceptions

  • How to use try, catch, and finally

  • How to handle different exception types

  • How to use throw to raise exceptions

  • How to create custom exceptions

  • How to validate input without relying unnecessarily on exceptions

  • Best practices for writing reliable C# applications

1. What Is an Exception in C#?

An exception is an object that represents an error or unexpected condition occurring while a program is running.

For example, consider the following code:

int number1 = 10;
int number2 = 0;

int result = number1 / number2;

Console.WriteLine(result);

This code attempts to divide an integer by zero. In C#, integer division by zero throws a DivideByZeroException.

If the exception is not handled, the current operation fails and the application may terminate, depending on how the program is executed and whether another part of the application handles the exception.

Exception handling allows you to respond to such conditions instead of letting the error go unhandled.

Common Exceptions in C#

Exception When It Occurs
DivideByZeroException An integer is divided by zero
FormatException A value cannot be converted into the requested format
NullReferenceException Code attempts to access a member through a null reference
IndexOutOfRangeException An array index is outside the valid range
FileNotFoundException A requested file cannot be found
InvalidOperationException An operation is invalid for the object's current state
ArgumentException An argument supplied to a method is invalid
IOException An input/output operation fails

The exact exception depends on the operation and the conditions under which it fails.

2. Errors vs. Exceptions

Errors can occur at different stages of development and execution.

Compile-Time Errors

Compile-time errors are detected by the compiler before the program can run successfully.

Example:

int number = "Hello";

This code produces a compilation error because a string cannot be assigned directly to an int variable.

Runtime Exceptions

Runtime exceptions occur while a program is executing.

Example:

int number = int.Parse("Hello");

The code compiles, but execution throws a FormatException because "Hello" cannot be parsed as an integer.

Important: Exception handling is designed to handle exceptional runtime conditions. It does not make invalid C# syntax or type errors compile successfully.

3. Understanding the Try-Catch Block

The try and catch statements allow you to execute code that might fail and handle an exception if one occurs.

Syntax

try
{
    // Code that may throw an exception
}
catch (Exception ex)
{
    // Handle the exception
}
  • try: Contains the code that may throw an exception.

  • catch: Handles a matching exception.

  • Exception ex: Stores a reference to the exception object, including its message and diagnostic information.

Example: Handling Division by Zero

using System;

class Program
{
    static void Main()
    {
        try
        {
            int number1 = 10;
            int number2 = 0;

            int result = number1 / number2;

            Console.WriteLine($"Result: {result}");
        }
        catch (DivideByZeroException ex)
        {
            Console.WriteLine("Error: Cannot divide by zero.");
        }

        Console.WriteLine("Program continues running.");
    }
}

Output:

Error: Cannot divide by zero.
Program continues running.

When division throws an exception, execution moves to the matching catch block. After the handler completes, execution continues after the try-catch statement.

The variable ex is available if you need diagnostic details. In production applications, avoid displaying raw exception details to end users.

4. Understanding the Finally Block

The finally block contains cleanup code that should run whether the associated try block completes successfully or an exception is handled.

Syntax

try
{
    // Code that may throw an exception
}
catch (Exception ex)
{
    // Handle an exception
}
finally
{
    // Cleanup code
}

The catch block is optional when a try statement has a finally block, but a try must have at least one catch or finally block.

Example

using System;

class Program
{
    static void Main()
    {
        try
        {
            int number1 = 20;
            int number2 = 0;

            int result = number1 / number2;

            Console.WriteLine(result);
        }
        catch (DivideByZeroException)
        {
            Console.WriteLine("Division by zero is not allowed.");
        }
        finally
        {
            Console.WriteLine("Execution reached the cleanup block.");
        }

        Console.WriteLine("Program finished.");
    }
}

Output:

Division by zero is not allowed.
Execution reached the cleanup block.
Program finished.

The finally block is useful for cleanup, but it should not be treated as an absolute guarantee under every possible termination scenario, such as abrupt process termination.

When Should You Use Finally?

Use finally when cleanup must occur after an operation, such as releasing a resource acquired manually.

For many .NET resources, however, using or await using is preferable because it expresses resource ownership clearly and ensures disposal through established language patterns.

5. Handling Multiple Exception Types

A single operation can fail in different ways. You can use multiple catch blocks to handle specific exception types differently.

Example

using System;

class Program
{
    static void Main()
    {
        try
        {
            Console.Write("Enter a number: ");
            int number = int.Parse(Console.ReadLine()!);

            int result = 100 / number;

            Console.WriteLine($"Result: {result}");
        }
        catch (FormatException)
        {
            Console.WriteLine("Please enter a valid whole number.");
        }
        catch (DivideByZeroException)
        {
            Console.WriteLine("The number cannot be zero.");
        }
        catch (OverflowException)
        {
            Console.WriteLine("The number is outside the supported range.");
        }
        finally
        {
            Console.WriteLine("Operation completed.");
        }
    }
}

Possible results:

  • Enter abc: the program handles a FormatException.

  • Enter 0: the program handles a DivideByZeroException.

  • Enter a whole number outside the supported int range: parsing can throw an OverflowException.

  • Enter 5: the program displays Result: 20.

The null-forgiving operator (!) tells the compiler that the expression is expected not to be null. It does not validate the actual input. In production code, input should be handled safely.

Why Should Specific Exceptions Come First?

A catch block for a more specific exception should appear before a catch block for a broader exception type that would also match it.

For example:

try
{
    int number = int.Parse("abc");
}
catch (FormatException)
{
    Console.WriteLine("Invalid number format.");
}
catch (Exception)
{
    Console.WriteLine("An unexpected error occurred.");
}

The first block handles the expected format problem. The general Exception handler catches other exceptions that reach it.

Avoid using a broad catch (Exception) merely to suppress errors. Handle exceptions where you can meaningfully recover, add context, or report the failure.

6. The Throw Keyword in C#

The throw keyword lets you raise an exception when a condition violates the rules of an operation.

Example: Validating Age

using System;

class Program
{
    static void ValidateAge(int age)
    {
        if (age < 18)
        {
            throw new ArgumentException(
                "Age must be at least 18.",
                nameof(age));
        }

        Console.WriteLine("Age validation successful.");
    }

    static void Main()
    {
        try
        {
            ValidateAge(16);
        }
        catch (ArgumentException ex)
        {
            Console.WriteLine($"Validation failed: {ex.Message}");
        }
    }
}

Output:

Validation failed: Age must be at least 18. (Parameter 'age')

The throw statement stops the current method's normal execution and searches for a matching handler up the call stack.

Throwing an Exception Is Not Always the Best Validation Strategy

For normal user input, methods such as int.TryParse() are often preferable because invalid input is expected rather than exceptional.

Use exceptions when an operation cannot proceed according to its contract, and use ordinary validation for common, expected conditions.

7. Rethrowing Exceptions Correctly

Sometimes a method catches an exception only to log information before allowing the original exception to propagate.

Use throw; to rethrow the currently handled exception while preserving its original stack trace.

try
{
    PerformOperation();
}
catch (Exception ex)
{
    Console.WriteLine($"Operation failed: {ex.Message}");
    throw;
}

Here, PerformOperation() represents a method defined elsewhere in the application.

Avoid this pattern:

catch (Exception ex)
{
    throw ex;
}

Using throw ex; resets the visible stack-trace origin to the rethrow location, making diagnosis more difficult. Prefer throw; when rethrowing the same exception.

Do not catch and rethrow an exception unless you have a useful reason to catch it, such as adding context, logging, or performing cleanup.

8. Creating Custom Exceptions

Built-in exceptions cover many situations, but an application may need to represent a specific business-rule failure.

For example, an online store may need an exception for an order that cannot be processed because the requested quantity is invalid.

A custom exception should normally derive from Exception or an appropriate exception subclass.

Example: Insufficient Balance Exception

using System;

public class InsufficientBalanceException : Exception
{
    public InsufficientBalanceException()
        : base("The account has insufficient balance.")
    {
    }

    public InsufficientBalanceException(string message)
        : base(message)
    {
    }
}

public class BankAccount
{
    public decimal Balance { get; private set; }

    public BankAccount(decimal initialBalance)
    {
        if (initialBalance < 0)
        {
            throw new ArgumentOutOfRangeException(
                nameof(initialBalance),
                "Initial balance cannot be negative.");
        }

        Balance = initialBalance;
    }

    public void Withdraw(decimal amount)
    {
        if (amount <= 0)
        {
            throw new ArgumentOutOfRangeException(
                nameof(amount),
                "Withdrawal amount must be positive.");
        }

        if (amount > Balance)
        {
            throw new InsufficientBalanceException(
                "Withdrawal declined: insufficient account balance.");
        }

        Balance -= amount;
        Console.WriteLine($"Withdrawal successful. Remaining balance: {Balance:C}");
    }
}

class Program
{
    static void Main()
    {
        BankAccount account = new BankAccount(1000m);

        try
        {
            account.Withdraw(1500m);
        }
        catch (InsufficientBalanceException ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

Output:

Withdrawal declined: insufficient account balance.

This example demonstrates how a custom exception can communicate a specific business failure. The account's balance is protected by a private setter, and the withdrawal amount is validated before the balance changes.

For a production library, custom exception types often include standard constructors for a message and an inner exception. Add these when appropriate for the library's needs.

9. Using TryParse Instead of Exceptions for User Input

A common beginner mistake is using int.Parse() for every user input and relying on exceptions to detect invalid values.

For routine input validation, int.TryParse() is often simpler.

Example

using System;

class Program
{
    static void Main()
    {
        Console.Write("Enter your age: ");
        string? input = Console.ReadLine();

        if (int.TryParse(input, out int age))
        {
            if (age >= 0)
            {
                Console.WriteLine($"Your age is {age}.");
            }
            else
            {
                Console.WriteLine("Age cannot be negative.");
            }
        }
        else
        {
            Console.WriteLine("Please enter a valid whole number.");
        }
    }
}

Example output:

Enter your age: twenty
Please enter a valid whole number.

TryParse returns true when the conversion succeeds and false when it fails. It does not throw a FormatException simply because the input is not a valid integer.

Use this approach for expected invalid input, and reserve exception handling for failures that require exceptional control flow.

10. Exception Handling with File Operations

File operations can fail because a file does not exist, access is denied, or the storage device is unavailable.

The following example demonstrates handling a missing file:

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string filePath = "data.txt";

        try
        {
            string content = File.ReadAllText(filePath);
            Console.WriteLine(content);
        }
        catch (FileNotFoundException)
        {
            Console.WriteLine("The requested file was not found.");
        }
        catch (UnauthorizedAccessException)
        {
            Console.WriteLine("You do not have permission to access this file.");
        }
        catch (IOException ex)
        {
            Console.WriteLine($"A file operation failed: {ex.Message}");
        }
    }
}

The order matters because FileNotFoundException is a type of IOException. The specific handler must appear before the broader I/O handler.

In a real application, consider whether the file should be created, whether a retry is appropriate, and how to log diagnostic details without exposing sensitive information.

11. Best Practices for Exception Handling

Follow these guidelines when writing maintainable C# code.

1. Catch Exceptions You Can Handle

Do not catch every exception unless you have a clear recovery, logging, or application-boundary strategy.

2. Prefer Specific Exception Types

Handle FormatException, IOException, or a domain-specific exception when you know how to respond to that failure.

3. Never Silently Ignore Exceptions

Avoid empty catch blocks:

catch (Exception)
{
    // Do nothing
}

Silently ignoring failures can hide bugs and leave the application in an inconsistent state.

4. Use Finally or Using for Cleanup

Prefer using for disposable resources such as file streams and database connections when the resource's lifetime fits a using scope.

using System.IO;

using (StreamReader reader = new StreamReader("data.txt"))
{
    string content = reader.ReadToEnd();
    System.Console.WriteLine(content);
}

The resource is disposed when execution leaves the using scope, including when an exception occurs.

5. Preserve Stack Traces

Use throw; rather than throw ex; when rethrowing the same exception.

6. Do Not Use Exceptions for Ordinary Control Flow

Use conditions, validation, and TryParse for expected situations such as an empty field or an invalid number.

7. Avoid Exposing Internal Details to Users

Detailed exception messages and stack traces can reveal implementation details. Show a clear, user-friendly message while recording appropriate diagnostic information securely.

8. Keep Catch Blocks Focused

A catch block should handle the failure it understands. If it cannot recover, it may need to log context and allow the exception to propagate.

12. Common Mistakes Beginners Should Avoid

  • Placing a general catch (Exception) before a more specific catch block.

  • Assuming that finally executes after every possible form of process termination.

  • Using throw ex; when rethrowing the same exception.

  • Treating TryParse failure as a reason to throw another exception unnecessarily.

  • Catching an exception and continuing when the application state may be invalid.

  • Displaying raw stack traces or sensitive exception information to ordinary users.

  • Creating custom exceptions for conditions that are already represented clearly by suitable built-in exceptions.

  • Using exceptions instead of simple if statements for predictable validation.

13. Practice Exercises

Try these exercises to strengthen your understanding.

Exercise 1: Write a program that divides two integers and handles DivideByZeroException.

Exercise 2: Ask the user to enter an integer. Use TryParse to validate the input.

Exercise 3: Create a program that reads a text file and handles a missing-file exception.

Exercise 4: Create a custom InvalidAgeException and throw it when a supplied age is outside your application's permitted range.

Exercise 5: Write a program containing try, catch, and finally. Observe which messages appear when the operation succeeds and when it throws an exception.

14. Knowledge Check

1. Which keyword defines the block containing code that may throw an exception?

A. check
B. try
C. error
D. validate

2. Which block is intended for cleanup code after a try statement?

A. final
B. cleanup
C. finally
D. end

3. Which exception commonly occurs when parsing a non-numeric string as an integer?

A. FormatException
B. DivideByZeroException
C. FileNotFoundException
D. IndexOutOfRangeException

4. Which statement correctly rethrows the currently handled exception while preserving its original stack-trace information?

A. throw ex;
B. throw;
C. return ex;
D. catch;

5. Which method is generally preferable for checking whether user input can be converted into an integer?

A. int.ForceParse()
B. int.ConvertAlways()
C. int.TryParse()
D. int.CheckNumber()

Answers

  1. B — try

  2. C — finally

  3. A — FormatException

  4. B — throw;

  5. C — int.TryParse()

15. Frequently Asked Questions

What is exception handling in C#?

Exception handling is a mechanism for responding to runtime failures using constructs such as try, catch, finally, and throw.

What is the difference between try and catch?

The try block contains code that may throw an exception. A matching catch block handles an exception when one occurs.

Is the finally block always executed?

A finally block normally runs when control leaves its associated try statement, including after a handled exception. It is not an absolute guarantee if the process terminates abruptly or execution is otherwise forcibly stopped.

Can a try block have multiple catch blocks?

Yes. Multiple catch blocks let you handle different exception types separately. More specific exception handlers must come before broader handlers that would also match them.

What is a custom exception in C#?

A custom exception is an application-defined exception type, usually derived from Exception or an appropriate exception subclass, that represents a specific failure condition.

What is the difference between throw and throw ex?

throw; rethrows the currently handled exception while preserving its original stack-trace information. throw ex; restarts the visible stack trace at the rethrow location and should generally be avoided for rethrowing the same exception.

Should I use exceptions to validate user input?

Not usually for expected input problems. Methods such as int.TryParse() and ordinary validation checks are generally more appropriate for routine user input.

Conclusion

Exception handling is an essential part of reliable C# development. By understanding try, catch, finally, throw, and custom exceptions, you can build applications that respond to runtime failures more predictably.

Remember to handle specific exceptions where recovery is possible, validate expected user input without unnecessary exceptions, preserve stack traces when rethrowing, and use using for resource cleanup when appropriate.

In the next tutorial, you will learn about C# Collections: List, Dictionary, HashSet and Queue, including how to store, organize, and work with groups of data efficiently.

←
Post 20 of 30
→