17 Feb 2020
Exceptions can happen anywhere. Some are thrown by your code, deliberately. Others are thrown by .NET or 3rd party libraries. You know where some exceptions may get thrown, and there are heaps more you didn’t even know existed.
Most likely you already know how
try ... catch ... finally block works (check out this MS
doco if you want to brush up on that), and while you understand it all at the high level, you
may still be wondering whether you should:
To make an informed judgment, you need to understand the main types of exceptions. This understanding will help you decide when to catch and how to handle individual kind of exception in a given situation.
Are you having issues with
NullReferenceException? Check out this article to get more specific
help: “How to avoid and fix NullReferenceExceptions in ASP.NET MVC”
From the perspective of what you can do with them, all exceptions can be divided into three large groups:
Believe it or not, but there are exceptions you can just ignore. Depending on the main of your application, you may decide to completely drop exceptions thrown by non-essential services, such as:
Services such as DataDog, Graphana, New Relic etc can receive data from your app to track CPU/RAM utilisation, performance metrics, latencies etc. Most of the time, it is safe to simply catch & ignore exceptions when calling those types of services.
If a call to log information/warning/error/failure messages throws exceptions, you may still decide that your app should nevertheless continue running and serving users’ requests.
However, there might be a significant downside to it – should your app actually encounter a fatal exception, there’ll be no trace of that anywhere in the logs.
There’s no one-size-fits-all answer, and you should make a call based on the type of your application and how critical it is to you to have logs. If you really must have them, all the time – then your app should definitely fail when it’s unable to log, and display a prominent error message/render a comprehensive error page.
Say you tried deleting a file, but that file is no longer there, and you get
DirectoryNotFoundException. As long as your app doesn’t need to do anything about it, you can just
ignore that exception.
You can, however, optimise your logic to check whether a file is still there before trying to delete it. Exceptions are expensive – the runtime spends quite a few CPU cycles to gather all the stack trace info and such when an exception is thrown. So if performance is important, try to avoid throwing, catching and ignoring no-op exceptions.
Whether you can ignore an exception largely depends on what’s important for your app – for instance, if logging or app metrics are a must, you may want your app to fail outright so that user could be notified of a problem.
You want to catch these exceptions as close to the point of invocation as possible so that no extra logic that your application is supposed to execute gets skipped.
Let’s look at a made-up problem. Imagine there are several ‘agent’ processes, that are running in
parallel, and as part of their work, they may need to delete a certain file. Conflicts may happen and
some agents may try to delete the same file, leading to the
FileNotFound exception being thrown.
In this instance, we want to catch that particular exception as soon as possible & just ignore it:
In a very few cases, you'd want to specify
Exception as the
generic exception type, so that every exception is ignored in this situation. By doing that you'll
ignore ALL exceptions, even the ones that you'd rather stop your app & let you know
something is wrong – like
OutOfMemoryException, or exceptions related to authorisation or
Swallowing all exceptions is troublesome and can lead to hard-to-debug situations, where your app crashes in some weird place with no clear indication of what's gone wrong.
In the brave new world of distributed services/microservice architecture, you can’t just assume that every network call to a service will succeed. There will be timeouts, disconnects, servers throttling the allocated capacity, DevOps people changing network setting and all sorts of crazy stuff. All of that can make your HTTP calls fail.
To address that, you can add retry logic when calling a service. Throw in a couple more smarts such as linear of exponential backing off (which is really a fancy way of saying “ok, you’re busy, I’ll come back after a fixed time/waiting for longer than before) and you’ve got yourself a retry policy!
Btw check out my other article How to call a JSON API and display the result in ASP.NET Core MVC for an example on how to implement a retry policy in ASP.NET Core.
When your logic to retrieve some data fails, you might be able to assume some safe to use value instead of failing.
You need to consider what’s worse in your individual situation: say, losing a customer because your app can’t retrieve a price for something, or assuming a sensible default price and carrying on with the checkout. You may want to talk to your business people - Product Managers or Business Analysts to decide what’s the acceptable level of risk and tradeoffs in an individual situation.
Things you can assume default values for:
These are the hardest to deal with. Generally, you should catch these exceptions and log all information in them, including call stack and error message. Also consider logging any context around them, such as user/object ids etc. This will help you troubleshoot these problems and fix the root cause - missing or incorrect data, configuration/connectivity issues, memory/performance issues, authentication/authorisation issues etc.
Ideally, there should be no exceptions. Practically, you will be able to eliminate some of the recurring ones by analysing your exception logs and coming up with a solution for that particular exception and leave some of the remaining ones as inevitable noise.
So, catch ALL types of exceptions at the highest level in your application. In the case of ASP.NET Core, you can add a custom Exception Handler Middleware which will catch errors occurring in your controllers.
Mind you that Exception Handling Middleware won’t catch any exceptions occurring during ASP.NET Core app startup, so for that you want to enable capture of startup errors.
For console apps, wrapping all calls in you
static void Main() with
catch should suffice:
In this article, you have just learnt
Let me know if you have any questions – just use the comments form below or send me an email! I reply to all emails I receive.
I hope this article helped you to take a step back and learn to identify various types of exceptions and how to deal with them.
Don’t miss my next post - subscribe to the mailing list to get handy tips and solutions for ASP.NET NVC Core. I never spam, and you can unsubscribe at any time.