

TL;DR
- Exceptions are the mechanism by which many programming languages model errors and unexpected situations.
- When handling an exception, you usually do common tasks such as logging, resource clean-up or disposal, and potentially retries.
- Not all exceptions are worth catching at the site where they happened.
- A good practice is having centralized middleware do exception handling for the whole application.
- The finally block in C# lets you run code that is guaranteed to be executed, regardless of the exception happening. It’s used for clean-up tasks that absolutely must happen.
In this post, you’ll learn the definition of an exception, what exception handling is, why it’s so important, and some best practices when handling exceptions.
Error handling is massively important in programming, to the point where some people say software development is mostly error handling. In many modern programming languages, the primary mechanism for managing errors is exception handling.
This guide is an introduction to exception handling. You’ll learn the definition of an exception, what exception handling is, and why it’s so important, and some best practices when handling exceptions. Additionally, we’ll show examples of exception handling in C#.
Before covering exception handling, though, it makes sense to offer a refresher on the concept of exceptions themselves.
Usually, the way an exception manifests itself during program execution is by interrupting the flow of execution
What is an exception?
An exception is a mechanism that many programming languages use to express a problem, error, or any unexpected situation.
Usually, the way an exception manifests itself during program execution is by interrupting the flow of execution. In other words, when an exception happens and it’s not handled, it crashes the program.
Additionally, there is usually a message that accompanies the exception, with the goal of explaining what went wrong.
Many modern programming languages—for instance, C#, Java, Python, and Ruby—model exceptions using classes. That is to say, they define one class to model a specific error or problem.
Taking C# as an example, these are common exception classes that represent common errors:
- DivideByZeroException
- NullReferenceException
- FileNotFoundException
- ArgumentOutOfRangeException
What is exception handling, and why is it important in programming?
What is exception handling?
Exception handling is the practice of writing special code that is able to detect when an exception happens, get all of the available information about the exception, and (potentially) prevent the program from crashing.
Before we go any further, let’s introduce a bit of terminology: When an exception happens, we say the exception has been “thrown” or “raised.” When we handle the exception, we “catch” it.
The way this is usually done in practice is by surrounding the code that might throw an exception with special exception-handling code. If the exception happens, the normal flow of execution is interrupted right away and jumps to the exception-handling part.
What to do when handling an exception
So, what does it mean to handle an exception? You’ve caught an exception, but then what happens next?
There are a variety of things you can do after catching an exception, and the actions you’ll take depend on the specifics of your application. Here are some common tasks that are usually done after catching an exception:
Logging
A very common thing to do when catching an exception is to log as much detail as possible about the error, because that will enable someone in the future to troubleshoot and fix whatever went wrong.
Rethrow
Sometimes it makes sense to rethrow the exception. One common scenario is for the code to throw a nested exception, passing the original exception along as an inner exception, and then adding a more descriptive error message that will make more sense to whatever gets to handle it last.
Return an error code or result type
Another common scenario is this: You catch the exception, log the details, and then return some form of result type that indicates that the attempted operation failed somehow.
Try again
There are situations in which the error might be transient and, if you wait for a bit and try again, the code will later succeed. In these situations, you can use the exception-handling block to implement retry logic.
Resource clean-up
There are resources that need to be properly closed or disposed of. Think of things like database connections or file handlers, for instance. The exception-handling part is often a good area of the code for doing this, even though it’s not the only one—more on this later.
In any serious application, there’s a vast array of things that can go wrong
Why is exception handling important?
In any serious application, there’s a vast array of things that can go wrong. Applications handle, validate, and parse huge amounts of data.
They interact with databases, third-party APIs, third-party packages, and libraries. They perform complex calculations. And generally speaking, they do a lot of tasks that might cause exceptions to happen one way or another.
Proper exception handling is essential to ensuring that your program is not only resilient against failures, but that it also degrades gracefully when they happen, providing friendly error messages to the end user and logging technical details to enable a technician/software engineer to troubleshoot and fix the issue.
A practical look at exception handling
Let’s now take a look at practical examples of exception handling in C#.
C# exception handling example #1
Let’s start with a simple console application that asks for a GitHub username and then shows some statistics about the entered user:
using Octokit;
using ProductHeaderValue = Octokit.ProductHeaderValue;
namespace ConsoleApp3;
class Program
{
static async Task Main()
{
var client = new GitHubClient(new ProductHeaderValue("MyApp"));
Console.WriteLine("Enter a GitHub username:");
string? username = Console.ReadLine();
Console.WriteLine($"Getting user info for {username}...");
User user = await client.User.Get(username);
Console.WriteLine($"API call successful! User: {user.Name} ({user.Login})");
Console.WriteLine($"Public repos: {user.PublicRepos}, Followers: {user.Followers}");
Console.WriteLine("Program finished.");
}
}
This is the result I get when I run the program. In the example below, I’m passing yyx990803< as the username, which is the username of Evan You, creator of the famous
Vue.js front-end framework:
Enter a GitHub username:
yyx990803
Getting user info for yyx990803...
API call successful! User: Evan You (yyx990803)
Public repos: 198, Followers: 104825
Program finished.
However, if I just enter some gibberish for the username, the code throws an exception.
More specifically, the code throws an exception of type Octokit.NotFoundException. That happens because what I typed isn’t a valid GitHub user. We need to handle that exception, so that we can show a friendly message when the entered user isn’t valid.
Let’s start by wrapping the offending code with a try block:
try
{
Console.WriteLine($"Getting user info for {username}...");
User user = await client.User.Get(username);
Console.WriteLine($"API call successful! User: {user.Name} ({user.Login})");
Console.WriteLine($"Public repos: {user.PublicRepos}, Followers: {user.Followers}");
Console.WriteLine("Program finished.");
}
This is C#’s way of declaring that that portion of code might throw an exception. The next step is to add the catch block, in which we’ll handle the exception.
We know the specific exception type we’re looking for, so we’ll catch it explicitly:
catch (NotFoundException ex)
{
Console.WriteLine($"User not found: {ex.Message}");
}
The code above catches the exception and assigns the exception object to the ex variable. Then, it prints a friendly error message along with the exception message coming from the exception object.
The way this works is that if the code inside the try block throws an exception of type Octokit.NotFoundException, the control flow is immediately transferred to the catch block. If an exception of another type happens, the catch block won’t catch it.
If I run the code and enter some nonsense as the username once more, I’ll get a different result:
Enter a GitHub username:
jkjgjhgjhg
Getting user info for jkjgjhgjhg...
User not found: Not Found
C# exception handling example #2
For this example, I’ll change the program:
static async Task Main()
{
var configuration = new ConfigurationBuilder()
.AddUserSecrets<Program>()
.Build();
string gitHubToken =
configuration.GetSection("GitHub:TOKEN").Value
?? "could not find token";
GitHubClient client = new(new ProductHeaderValue("MyApp"))
{
Credentials = new Credentials(gitHubToken)
};
var newRepo = new NewRepository("exception-handling-post");
await client.Repository.Create(newRepo);
Console.WriteLine("Repository created successfully.");
}
The code now does the following:
- Sets up a configuration from the user secrets .NET feature
- Gets a PAT (personal access token) from the configuration
- Instantiates a GitHub client object using the PAT for the credentials
- Creates a new repository object
- Tries to create a new repo on GitHub
- Displays a success message
When I run that code, I get an Octokit.AuthorizationException with the message “Bad credentials.” So, let’s add a try catch block for that possibility:
try
{
string gitHubToken =
configuration.GetSection("GitHub:TOKEN").Value
?? "could not find token";
GitHubClient client = new(new ProductHeaderValue("MyApp"))
{
Credentials = new Credentials(gitHubToken)
};
var newRepo = new NewRepository("exception-handling-post");
await client.Repository.Create(newRepo);
Console.WriteLine("Repository created successfully.");
}
catch (AuthorizationException ex)
{
Console.WriteLine(
$"Something went wrong trying to authenticate to GitHub: {ex.Message}"
);
}
Now, when I run the code, I see this:
Something went wrong trying to authenticate to GitHub: Bad credentials
As it turns out, the code has a bug that needs to be fixed: I’m trying to retrieve the PAT from the configuration using the wrong key. Where it says GitHub:TOKEN it should’ve said GitHub:PAT.
Once that’s fixed, I start getting a different error:
Octokit.RepositoryExistsException: ‘There is already a repository named ‘exception-handling-post’ for the current account.’
That’s pretty self-explanatory and easy to fix. But let’s first handle the exception, adding yet another catch block:
catch (RepositoryExistsException ex)
{
Console.WriteLine($"Repository already exists: {ex.Message}");
}
Now, I can simply change the name of the repository to something that is guaranteed to be unique, and then we’ll no longer have errors.
When to catch vs. when not to catch exceptions
Not all exceptions are worth catching. Some of them you really ought to let bubble up unhandled. Whereas others, you should catch as close as possible to the place when they happened and handle them quickly. Let’s see how you can tell those apart.
DO NOT catch exceptions that can be prevented by writing better code
You should never catch exceptions that happen because your code isn’t defensive enough. Instead, you should write better code so that the exception can’t happen in the first place.
An example will make this clearer. Suppose you have some C# code that attempts to get a port number out of a URL:
private static void PrintPortNumber(string url)
{
var parts = url.Split(':');
var port = parts[2];
Console.WriteLine($"Port number: {port}");
}
The code above is not robust. It works, but only as long as you pass a well-formed URL such as “https://localhost:5000”.
If the string passed contains fewer than two colons, then the line that attempts to retrieve the element with index 2 from the array will fail, and an exception will happen:
Unhandled Exception:
System.IndexOutOfRangeException: Index was outside the bounds of the array.
The wrong way to fix this issue is to handle the exception. So, don’t do the following:
// BAD CODE, don't do it this way!!!
try
{
var parts = url.Split(':');
var port = parts[2];
Console.WriteLine($"Port number: {port}");
}
catch (IndexOutOfRangeException)
{
Console.WriteLine("Url was in invalid format.");
}
The problem here is that the exception only happens because the code isn’t defensive and doesn’t validate the URL before attempting to get the port.
You could verify that the parts variable has a length of at least 3 before attempting to get the last element. Or, before splitting, you check that the string has 3 colons.
It’s better, though, to just use the System.Uri class from .NET, which handles this type of validation very efficiently:
private static void PrintPortNumber(string url)
{
if (!Uri.IsWellFormedUriString(url, UriKind.Absolute))
{
Console.WriteLine("Url is not well formed!");
return;
}
Uri uri = new(url);
if (uri.IsDefaultPort)
{
Console.WriteLine("No custom port was defined!");
return;
}
Console.WriteLine($"Port number: {uri.Port}");
}
If I then call the method three times, like this:
PrintPortNumber("clearly not a valid url!");
PrintPortNumber("https://localhost");
PrintPortNumber("https://localhost:5000");
I get the following results:
Url is not well formed!
No custom port was defined!
Port number: 5000
Eric Lippert—former principal developer at the C# compiler team—calls these types of exceptions “boneheaded exceptions”:
DO CATCH exceptions caused by poor third-party design decisions—unless you can prevent them
Generally, you shouldn’t throw exceptions for situations that are expected. Instead, you should reserve them for truly exceptional scenarios.
Unfortunately, not everyone follows this to the letter. Sometimes, you’ll find yourself integrating with code from a third-party package that throws exceptions for non-exceptional situations, where they should have probably returned a boolean or some kind of result type.
I think that SDKs (software development kits) that wrap over REST APIs should not throw exceptions for most non-200 status codes.
Even though a 404 NOT FOUND result is not successful, it’s often an expected result. Thus, I think those libraries should return result types rather than throw.
So, the bad example for this section is actually the GitHub example we saw earlier. I think Octokit there made a bad design decision in throwing a 404. You have to catch the exception, though, in that circumstance, even though you wish you didn’t have to.
Let’s now see an example of a scenario where you can prevent this situation. Consider the following method:
private static void PrintEvenOrOdd(string numberCandidate)
{
try
{
int number = int.Parse(numberCandidate);
Console.WriteLine($"{number} is {(number % 2 == 0 ? "even" : "odd")}");
}
catch (FormatException)
{
Console.WriteLine($"Argument {numberCandidate} is not a valid number.");
}
}
In the example above, you can’t help but catch the exception, since the int.Parse method throws when the candidate string does not represent a valid integer number.
This is an unfortunate design decision by the .NET developers back then, which forces consumer calls to use exceptions for flow control, which is something you should avoid doing.
The good news is: you don’t have to use int.Parse. Instead, use int.TryParse, which is the newest version of the method that returns a boolean instead:
private static void PrintEvenOrOdd(string numberCandidate)
{
if (!int.TryParse(numberCandidate, out int number))
{
Console.WriteLine($"Argument {numberCandidate} is not a valid number.");
return;
}
Console.WriteLine($"{number} is {(number % 2 == 0 ? "even" : "odd")}");
}
This is a scenario in which you can avoid catching these “unfortunate” exceptions, so do avoid them here.
Generally, you shouldn’t throw exceptions for situations that are expected.
DO NOT catch exceptions just to rethrow them
Do not do the following:
public IEnumerable<Product> GetProducts()
{
try
{
return _db.Products.ToList();
}
catch (SqlException ex)
{
throw ex;
}
}
Here, you’re just rethrowing the exception with no value added. So, it’s like doing nothing, except it’s worse. Because when doing throw ex; you reset the stack trace of the exception, losing vital debugging information.
The code above pays a performance penalty because the try-catch block erases debugging information and doesn’t do anything useful with the exception.
If you need to rethrow an exception, the best practice is to wrap them in a domain-specific exception, with a friendlier error message, and keep the original exception as an internal exception. For instance:
throw new DataAccessException("Failed to retrieve products.", ex);
Having a try-catch with an empty catch block is a terrible practice because you’re just swallowing an error with no hope of ever getting the details of what happened.
DO NOT swallow exceptions
Having a try-catch with an empty catch block is a terrible practice because you’re just swallowing an error with no hope of ever getting the details of what happened.
In extraordinary cases, you might have the need to silence an error—especially when dealing with the “unfortunate design choices” type of exceptions we mentioned earlier—but at the very least, log a warning and leave a descriptive comment explaining the reason for your choice.
DO use a centralized exception handling middleware
For many applications, web apps particularly, what you should do is set up a way of automatically catching and handling all unhandled exceptions that make it to the top level. Then, what do you do with the exception?
What makes sense in most cases is to log the exception details and return a friendly-but-vague error message to the end user. If it’s an API, you return a 500 Internal Server Error code and maybe a small payload with the error message.
In practice, most of the exceptions will end up here, because most of them belong to the categories that don’t make sense to handle at the spot they happened.
The finally block in C#
All of the examples in this post so far have been using the try-catch block. That’s the most common way of handling exceptions in C#. There’s also another important keyword you need to learn: the finally keyword.
Using the finally keyword, you can write try-catch-finally and try-finally blocks. You’ll learn now about the two.
The try-catch-finally block
You use the try-catch-finally block when you need to run some code regardless of an error happening or not.
The classic scenario for this is when you have resources that you need to clean, dispose, close, or otherwise “finalize” in some way, under penalty of issues down the road, such as memory leaks.
In such scenarios, you want to make sure that this final clean-up always happens, for both the happy- and the sad-path scenarios. Here’s an example:
public string? ReadFile(string path)
{
StreamReader? reader = null;
try
{
reader = new StreamReader(path);
return reader.ReadToEnd();
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"File not found: {ex.Message}");
return null;
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine($"Access denied: {ex.Message}");
return null;
}
finally
{
reader?.Dispose();
}
}
The try-finally block
Another variation of the example above is when you don’t have anything useful to do with the exception, so you really want it to bubble up the chain call, but you want to make sure resources are cleaned or disposed of.
In that case, you’d use the try-finally block, and the flow of execution then becomes:
- Start executing the code inside the try block
- When the exception happens, execution is interrupted
- Control is handed over to the finally block, which runs to completion
- The exception is then effectively thrown
In practice, in many situations, you no longer need the finally keyword. That’s because you can leverage the using keyword, which handles disposing for you under the hood.
So, code like this:
var reader = new StreamReader(path);
try
{
return reader.ReadToEnd();
}
finally
{
reader.Dispose();
}
Can be safely replaced by this:
using var reader = new StreamReader(path);
return reader.ReadToEnd();
A caveat though: the using keyword approach is something that only works for objects that are “disposable”—that is, they implement the IDisposable interface.
The finally block is still very much needed in other scenarios, such as freeing locks, doing essential clean-ups, or closing connections that don’t implement IDisposable.
Here’s a diagram that shows the entire flow of the try-catch-finally block:

Conclusion
This post was an introductory guide to exception handling, with examples in C#. You’ve learned what exceptions are, what exception handling is, why it matters, and how it works.
But there’s much more to it than that. As a next step, we encourage you to keep researching.
Here are some topics you might be interested in:
- How to create custom exception classes, and when you should do it
- Best practices when logging exceptions
- When to catch and when not to catch exceptions
- The finally block in C#
As your project scales up, you can explore tools that will help you automate the validation of exception handling.
Platforms like Tricentis Tosca can ensure that your applications correctly respond to both unexpected and expected errors. Thus, helping you catch unhandled exceptions early in the development or testing phase.
