Try-Catch Blocks - Handling Exceptions Gracefully
Vaibhav • September 10, 2025
In the previous article, we introduced the concept of exceptions-runtime events that signal something has gone
wrong. You saw how exceptions differ from regular errors, and why handling them is essential for writing robust,
production-grade software. Now it’s time to explore the core mechanism C# provides for dealing with exceptions:
the try
and catch
blocks.
These blocks form the backbone of structured exception handling in C#. They let you isolate risky code and
respond to failures in a controlled way. In this article, we’ll walk through how each block works, how
exceptions propagate, and how to write handlers that are both safe and intentional. We’ll avoid discussing finally
here, as it’s covered in a dedicated article later in this chapter.
What is a Try-Catch Block?
A try
-catch
block is a control structure that
wraps code which might throw an exception. If an exception occurs inside the try
block, control jumps to the matching catch
block. If no exception occurs, the catch
block is skipped entirely.
// Basic try-catch example
try
{
int x = 10;
int y = 0;
int result = x / y; // This will throw DivideByZeroException
Console.WriteLine("Result: " + result);
}
catch (DivideByZeroException)
{
Console.WriteLine("Cannot divide by zero.");
}
In this example, the division by zero triggers a DivideByZeroException
. The
catch
block intercepts it and prints a friendly message instead of crashing the
program. If y
had been a non-zero value, the catch
block would have been skipped.
You can catch specific exception types to handle different failure modes differently. This makes your error handling more precise and intentional.
Multiple Catch Blocks
Sometimes, different exceptions require different responses. C# lets you stack multiple catch
blocks to handle each type separately. The runtime matches the first
compatible block and skips the rest.
try
{
string input = null;
Console.WriteLine(input.Length); // NullReferenceException
}
catch (DivideByZeroException)
{
Console.WriteLine("Math error.");
}
catch (NullReferenceException)
{
Console.WriteLine("Object was null.");
}
catch (Exception ex)
{
Console.WriteLine("Unexpected error: " + ex.Message);
}
Here, the NullReferenceException
is caught by its matching block. If the
exception had been something else, like FormatException
, it would fall through
to the generic Exception
block.
Always place more specific catch
blocks before
general ones. Otherwise, the general block will intercept exceptions that could have been handled more
precisely.
Inspecting the Exception Object
Every exception object carries useful information: a message, a stack trace, and sometimes inner exceptions. You
can access these via the Exception
parameter in your catch
block.
try
{
int[] numbers = new int[2];
Console.WriteLine(numbers[5]); // IndexOutOfRangeException
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex.Message);
Console.WriteLine("Stack Trace: " + ex.StackTrace);
}
This pattern is especially useful for logging and diagnostics. You can record the exact error and where it
occurred, which helps during debugging or when analyzing crash reports. The Message
property gives a human-readable description, and StackTrace
shows where the exception originated.
Exception Propagation
If an exception isn’t caught in the current method, it propagates up the call stack until it finds a matching
catch
block. If none is found, the program terminates.
void Level1()
{
Level2();
}
void Level2()
{
Level3();
}
void Level3()
{
throw new Exception("Boom!");
}
try
{
Level1();
}
catch (Exception ex)
{
Console.WriteLine("Handled at top level: " + ex.Message);
}
The exception thrown in Level3
bubbles up through Level2
and Level1
until it’s caught at the top
level. This is called exception propagation. It allows you to centralize error handling in higher-level
methods.
You can rethrow an exception using throw;
inside a catch
block. This preserves the original stack trace and lets
higher-level handlers deal with it.
Throwing Your Own Exceptions
You’re not limited to catching exceptions-you can throw them too. This is useful when your method detects an invalid state and wants to signal failure to the caller.
void ValidateAge(int age)
{
if (age < 0)
{
throw new ArgumentOutOfRangeException("Age cannot be negative.");
}
}
This method checks for invalid input and throws an exception if the condition is violated. The caller can then catch and handle it appropriately. Throwing exceptions is a way to enforce contracts and signal serious problems that cannot be handled locally.
Common Mistakes to Avoid
Exception handling is powerful, but it’s easy to misuse. Here are some patterns to avoid:
// ❌ Catching all exceptions without handling
try
{
// risky code
}
catch
{
// silently ignore
}
// ❌ Catching Exception but doing nothing
catch (Exception)
{
// no logging, no message
}
// ❌ Using exceptions for control flow
try
{
int value = int.Parse("abc"); // throws FormatException
}
catch (FormatException)
{
value = 0; // fallback
}
These patterns hide problems instead of solving them. Always handle exceptions intentionally, and never use them
as a substitute for regular control flow. For example, prefer int.TryParse
over
catching FormatException
when parsing user input.
Validate inputs before risky operations. Use exceptions for truly exceptional cases-not for routine checks or expected conditions.
Summary
In this article, you learned how to use try
and catch
blocks to handle exceptions in C#. You saw how to catch specific exception
types, inspect exception objects, and throw your own when needed. You also learned how exceptions propagate
through the call stack and how to avoid common pitfalls like catching everything or using exceptions for control
flow.
Exception handling is a key part of writing resilient software. It lets your program respond to failures gracefully, preserve user experience, and maintain system integrity. In the next article, we’ll explore Finally Blocks: how to use them for cleanup, resource management, and ensuring code runs regardless of exceptions.