DEV Community

Maysam Ehab Al-Qrinawi
Maysam Ehab Al-Qrinawi

Posted on

What I Learned About Exceptions in C#: Understanding Errors Instead of Hiding Them

When I started learning C#, I thought exceptions were mainly about one thing:

Put the code inside try, catch the exception, and the problem is solved.

But as I worked more with .NET and started building APIs, I realized that handling an exception is not the same as hiding it.

This article is about what I learned while trying to understand exceptions in C# and how my way of thinking about errors changed.


What is an Exception?

An exception is an unexpected situation that happens while a program is running.

For example:

int number = int.Parse("hello");
Enter fullscreen mode Exit fullscreen mode

This code cannot convert "hello" into an integer, so C# throws a FormatException.

Another example:

string name = null;

Console.WriteLine(name.Length);
Enter fullscreen mode Exit fullscreen mode

Trying to access Length when name is null can result in a NullReferenceException.

These errors are not necessarily bad.

They are information.

They tell us:

"Something happened that your program needs to deal with."


My First Approach: Catch Everything

At first, I thought something like this was a good solution:

try
{
    DoSomething();
}
catch (Exception)
{
}
Enter fullscreen mode Exit fullscreen mode

The application doesn't crash.

The error disappears.

So... problem solved?

Not really.

The problem is that I didn't actually handle anything.

I just told the application:

"If something goes wrong, ignore it."

And this can make debugging much harder.

If something fails in production, I might not even know that it happened.


So What Should We Do Instead?

The first thing I learned is:

Don't catch an exception unless you know what you are going to do with it.

For example, if I'm parsing user input:

try
{
    int age = int.Parse(input);
}
catch (FormatException)
{
    Console.WriteLine("Please enter a valid number.");
}
Enter fullscreen mode Exit fullscreen mode

Here, catching FormatException makes sense.

I know what the problem means, and I know how to respond to it.

This is much better than:

catch (Exception)
{
    Console.WriteLine("Something went wrong.");
}
Enter fullscreen mode Exit fullscreen mode

The more specific the exception, the more meaningful the handling can be.


What About ArgumentNullException?

While learning C#, I also came across exceptions such as:

ArgumentNullException
Enter fullscreen mode Exit fullscreen mode

At first, I wondered:

Why create a specific exception when I can just use **Exception***?*

The answer became clearer when I understood that exceptions communicate the type of problem.

For example:

public void CreateUser(string name)
{
    if (name == null)
    {
        throw new ArgumentNullException(nameof(name));
    }

    // Continue...
}
Enter fullscreen mode Exit fullscreen mode

Here I'm saying:

"The caller provided a null argument where a value was required."

That's much more informative than:

throw new Exception("Something went wrong.");
Enter fullscreen mode Exit fullscreen mode

The exception itself carries useful information about the problem.


Throwing an Exception Is Not the Same as Catching It

This was another important thing I learned.

We can throw an exception when something invalid happens:

if (amount <= 0)
{
    throw new ArgumentException("Amount must be greater than zero.");
}
Enter fullscreen mode Exit fullscreen mode

And another layer of the application can decide whether it needs to catch and handle that exception.

For example:

Controller
    ↓
Service
    ↓
Business Logic
    ↓
Exception
Enter fullscreen mode Exit fullscreen mode

The service might detect the problem, but the appropriate place to convert that error into an HTTP response could be higher in the application.

This made me start thinking about exceptions as part of the application's flow rather than just something that belongs inside a try/catch.


Exceptions in ASP.NET Core

When I started working with ASP.NET Core APIs, this became even more important.

Imagine an API endpoint:

[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
    var user = await _userService.GetByIdAsync(id);

    return Ok(user);
}
Enter fullscreen mode Exit fullscreen mode

If something unexpected happens inside the service, I don't necessarily want every controller to contain:

try
{
    // ...
}
catch (Exception)
{
    // ...
}
Enter fullscreen mode Exit fullscreen mode

That would quickly become repetitive.

Instead, ASP.NET Core applications can use global exception handling.

For example, exception-handling middleware can act as a central place to handle unexpected exceptions.

Conceptually:

Request
   ↓
Controller
   ↓
Service
   ↓
Exception
   ↓
Global Exception Handler
   ↓
HTTP Response
Enter fullscreen mode Exit fullscreen mode

This approach helped me understand an important principle:

Handle errors at the right level, not everywhere.


Should We Create Custom Exceptions?

Sometimes the built-in exceptions are enough.

But in some applications, we may want exceptions that represent specific business problems.

For example:

public class UserNotFoundException : Exception
{
    public UserNotFoundException(string message)
        : base(message)
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

Then:

if (user == null)
{
    throw new UserNotFoundException("User was not found.");
}
Enter fullscreen mode Exit fullscreen mode

Now the exception represents a specific situation in the application.

However, I also learned that custom exceptions shouldn't be created just because we can.

If an existing .NET exception clearly describes the problem, using it can be simpler and clearer.


What I Learned About Error Messages

I used to think the exception type was enough.

But the message matters too.

Compare:

throw new Exception("Error");
Enter fullscreen mode Exit fullscreen mode

with:

throw new ArgumentException(
    "Amount must be greater than zero.",
    nameof(amount));
Enter fullscreen mode Exit fullscreen mode

The second one gives much more useful information.

A good error message should help us understand:

  • What went wrong?
  • Which value caused the problem?
  • What was expected?

But we should also be careful not to expose sensitive information to API users.


Exceptions Shouldn't Replace Validation

Another thing I'm learning is that not every invalid input should necessarily become an exception.

For example, if an API receives:

{
    "age": -5
}
Enter fullscreen mode Exit fullscreen mode

This is expected invalid user input.

It may be better to validate the request and return a validation response rather than using exceptions for normal application flow.

Exceptions are more appropriate for unexpected or exceptional situations, while validation handles expected invalid input.

This distinction helped me understand exceptions much better.


What I Would Do Differently Now

If I were starting again, I would remember these rules:

1. Don't catch exceptions just to make them disappear

catch (Exception)
{
}
Enter fullscreen mode Exit fullscreen mode

usually doesn't solve the real problem.

2. Catch specific exceptions when you can handle them

catch (FormatException)
{
    // Handle invalid format
}
Enter fullscreen mode Exit fullscreen mode

is more meaningful.

3. Use the right built-in exception

For example:

ArgumentNullException
ArgumentException
InvalidOperationException
Enter fullscreen mode Exit fullscreen mode

when they accurately describe the situation.

4. Don't use exceptions for normal validation

Expected invalid input should usually be handled through validation.

5. Think about where the exception should be handled

Not every exception needs a try/catch in every method.

6. Don't expose sensitive internal details

The error returned to the client should not reveal things like database connection strings, stack traces, passwords, or internal implementation details.


The Biggest Lesson

The biggest thing I learned is that exceptions are not just errors that we need to hide.

They are a way for different parts of an application to communicate that something unexpected happened.

The goal isn't:

"How can I stop this exception from appearing?"

The better question is:

"What does this exception tell me, and where should this problem be handled?"

I'm still learning .NET, and I'm sure my understanding of exception handling will continue to evolve as I build larger applications.

But this small change in perspective made exception handling much clearer for me.


Final Thought

When you're learning programming, it's easy to focus on making the code work.

But as you build larger applications, you start asking different questions:

  • What happens when something fails?
  • Where should I handle it?
  • How can I make debugging easier?
  • What information should the user receive?
  • How can I keep the application maintainable?

For me, understanding exceptions was one of those small topics that turned into a bigger lesson about writing reliable software.

And I'm still learning. 🚀


What was the biggest lesson you learned about exception handling in C#?

I'd love to hear your thoughts.

Top comments (0)