Top 10 Exception Handling Mistakes in .NET (and How to Actually Fix Them)
Every .NET developer has been there it's 3 AM, production just went down, and the logs are flooding in.
You open the error trace, only to find⦠nothing useful. The stack trace starts halfway through a catch block, or worse it's empty. Somewhere, an innocent-looking throw ex;
or a swallowed background exception has just cost hours of sleep.
Exception handling is one of those things that seems simple on the surface but can quietly undermine an entire system if done wrong. Tiny mistakes like catching Exception
, forgetting an await
, or rethrowing incorrectly don't just break code; they break observability. They hide root causes, produce misleading logs, and make even well-architected applications feel unpredictable.
In this article, we'll go through the most common exception handling mistakes developers make in .NET and more importantly, how to fix them. Along the way, you'll see how small choices in your code can mean the difference between a five-minute fix and a full-blown production nightmare.
𧨠1. Catching Exception
(and Everything Else)
The mistake:
try
{
// Some operation
}
catch (Exception ex)
{
// Just to be safe
}
Why it's a problem:
Catching the base Exception
type hides all context including OutOfMemoryException
, StackOverflowException
, and other runtime-level issues that you should never handle manually. It also makes debugging painful since you lose the ability to treat specific failures differently.
The right way:
Catch only what you can handle:
catch (SqlException ex)
{
// Handle DB issues
}
catch (IOException ex)
{
// Handle file issues
}
If you really must catch all exceptions (e.g., at a system boundary), log and rethrow:
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error occurred");
throw;
}
π‘ ABP Tip: In ABP-based applications, you rarely need to catch every exception at the controller or service level.
The framework's built-inAbpExceptionFilter
already handles unexpected exceptions, logs them, and returns standardized JSON responses automatically keeping your controllers clean and consistent.
π³οΈ 2. Swallowing Exceptions Silently
The mistake:
try
{
DoSomething();
}
catch
{
// ignore
}
Why it's a problem:
Silent failures make debugging nearly impossible. You lose stack traces, error context, and sometimes even awareness that something failed at all.
The right way:
Always log or rethrow, unless you have a very specific reason not to:
try
{
_cache.Remove(key);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to clear cache key {Key}", key);
}
π‘ ABP Tip: Since ABP automatically logs all unhandled exceptions, it's often better to let the framework handle them. Only catch exceptions when you want to enrich logs or add custom business logic before rethrowing.
π 3. Using throw ex;
Instead of throw;
The mistake:
catch (Exception ex)
{
Log(ex);
throw ex;
}
Why it's a problem:
Using throw ex;
resets the stack trace you lose where the exception actually occurred. This is one of the biggest causes of misleading production logs.
The right way:
catch (Exception ex)
{
Log(ex);
throw; // preserves stack trace
}
βοΈ 4. Wrapping Everything in Try/Catch
The mistake:
Developers sometimes wrap every function in try/catch βjust to be safe.β
Why it's a problem:
This clutters your code and hides the real source of problems. Exception handling should happen at system boundaries, not in every method.
The right way:
Handle exceptions at higher levels (e.g., middleware, controllers, background jobs). Let lower layers throw naturally.
π‘ ABP Tip: The ABP Framework provides a top-level exception pipeline via filters and middleware. You can focus purely on your business logic ABP automatically translates unhandled exceptions into standardized API responses.
π 5. Using Exceptions for Control Flow
The mistake:
try
{
var user = GetUserById(id);
}
catch (UserNotFoundException)
{
user = CreateNewUser();
}
Why it's a problem:
Exceptions are expensive and should represent unexpected states, not normal control flow.
The right way:
var user = GetUserByIdOrDefault(id) ?? CreateNewUser();
πͺ 6. Forgetting to Await Async Calls
The mistake:
try
{
DoSomethingAsync(); // missing await!
}
catch (Exception ex)
{
...
}
Why it's a problem:
Without await
, the exception happens on another thread, outside your try/catch
. It never gets caught.
The right way:
try
{
await DoSomethingAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during async operation");
}
π§΅ 7. Ignoring Background Task Exceptions
The mistake:
Task.Run(() => SomeWork());
Why it's a problem:
Unobserved task exceptions can crash your process or vanish silently, depending on configuration.
The right way:
_ = Task.Run(async () =>
{
try
{
await SomeWork();
}
catch (Exception ex)
{
_logger.LogError(ex, "Background task failed");
}
});
π¦ 8. Throwing Generic Exceptions
The mistake:
throw new Exception("Something went wrong");
Why it's a problem:
Generic exceptions carry no semantic meaning. You can't catch or interpret them specifically later.
The right way:
Use more descriptive types:
throw new InvalidOperationException("Order is already processed");
π‘ ABP Tip: In ABP applications, you can throw a
BusinessException
orUserFriendlyException
instead.
These support structured data, error codes, localization, and automatic HTTP status mapping:throw new BusinessException("App:010046") .WithData("UserName", "john");
This integrates with ABP's localization system, letting your error messages be translated automatically based on the error code.
πͺ 9. Losing Inner Exceptions
The mistake:
catch (Exception ex)
{
throw new CustomException("Failed to process order");
}
Why it's a problem:
You lose the inner exception and its stack trace the real reason behind the failure.
The right way:
catch (Exception ex)
{
throw new CustomException("Failed to process order", ex);
}
π‘ ABP Tip: ABP automatically preserves and logs inner exceptions (for example, inside
BusinessException
chains). You don't need to add boilerplate to capture nested errors just throw them properly.
π§ 10. Missing Global Exception Handling
The mistake:
Catching exceptions manually in every controller.
Why it's a problem:
It creates duplicated logic, inconsistent responses, and gaps in logging.
The right way:
Use middleware or a global exception filter:
app.UseExceptionHandler("/error");
π‘ ABP Tip: ABP already includes a complete global exception system that:
Logs exceptions automatically
Returns a standard
RemoteServiceErrorResponse
JSON objectMaps exceptions to correct HTTP status codes (e.g., 403 for business rules, 404 for entity not found, 400 for validation)
Allows customization through
AbpExceptionHttpStatusCodeOptions
You can even implement anExceptionSubscriber
to react to certain exceptions (e.g., send notifications or trigger audits).
π§© Bonus: Validation Is Not an Exception
The mistake:
Throwing exceptions for predictable user input errors.
The right way:
Use proper validation instead:
[Required]
public string UserName { get; set; }
π‘ ABP Tip: ABP automatically throws an
AbpValidationException
when DTO validation fails.
You don't need to handle this manually ABP formats it into a structured JSON response withvalidationErrors
.
π§ Final Thoughts
Exception handling isn't just about preventing crashes it's about making your failures observable, meaningful, and recoverable.
When done right, your logs tell a story: what happened, where, and why.
When done wrong, you're left staring at a 3 AM mystery.
By avoiding these common pitfalls and taking advantage of frameworks like ABP that handle the heavy lifting you'll spend less time chasing ghosts and more time building stable, predictable systems.
Comments
No one has commented yet, be the first to comment!