Low level built-in exception types offer little context and are much harder to diagnose than custom exceptions that can use the language of the model or application.
Low level built-in exception types offer little context and are much harder to diagnose than custom exceptions that can use the language of the model or application.
Thanks to DevIQ for sponsoring this episode!
Given the choice, avoid throwing basic exception types like Exception
, ApplicationException
, and SystemException
from your application code. Instead, create your own exception types that inherit from System.Exception
. You can also catch common but difficult-to-diagnose exceptions like NullReferenceException
and wrap them in your own application-specific exceptions. You should think about your application exceptions as being part of your domain model. They represent known bad states that your system can find itself in or having to deal with. You should be able to use your ubiquitous language to discuss these exceptions and their sources within the system with your non-technical domain experts and stakeholders. Let's talk about a few different examples.
Consider some code that does the following:
public decimal CalculateShipping(string zipCode){ var area = GetAreaFromZipcode(zipcode); if (area == null) { throw new Exception("Unknown ZIP Code"); } // perform shipping calculation}
The problem with this kind of code is, client code that attempts to catch exceptions resulting from the shipping calculation are forced to catch generic Exception
instances, instead of a more specific exception type. It takes very little code to create a custom exception type for application-specific exceptions like this one:
public class UnknownZipCodeException : Exception{ public string ZipCode { get; private set; } public UnknownZipCodeException(string message, string zipCode) : base(message) { ZipCode = zipCode; }}
In fact, in many cases you can create an overload that sets a standard default exception message, so you're consistent and your code is more expressive with fewer magic strings. Add this overload to the above exception, for instance:
public UnknownZipCodeException(string zipCode) :this("Unknown ZIP Code",zipCode){}
And now the original code can change to:
public decimal CalculateShipping(string zipCode){ var area = GetAreaFromZipcode(zipcode); if (area == null) { throw new UnknownZipCodeException(zipCode); } // perform shipping calculation}
Now client code can easily catch and handle the UnkownZipcodeException
type, resulting in a more robust and intuitive design.
An easy way to make your software easier to work with, both for your users and for developers, is to use higher level custom exceptions instead of low level exceptions. Low level exceptions like NullReferenceException
should rarely be returned from business-level classes, where most of your custom logic should reside. By using custom exceptions, you make it much more clear to everybody involved what the actual problem is. You're working at a higher abstraction level, using the language of the business domain.
For example, let’s say you’re writing an application that works with a database. Perhaps it’s an ASP.NET Core application in the medical or insurance industry, and it references individual customers as Subjects. Within some business logic dedicated to creating an invoice, recording a prescription, or filing a claim, there’s a reference to the Subject Id that is invalid. When your data layer makes the request and returns from the database, the result is empty.
var subject = GetSubject(subjectId);subject.DoSomething();
Obviously in this code, if Subject is null, the last line is going to throw an exception (you can avoid this by using the Null Object Pattern). Let’s further assume that we can’t handle this exception here – if the subject id is incorrect, there’s nothing else for this method to do but throw an exception, since it was going to return the subject otherwise. The current behavior for a user, tester, or developer is this:
Unhandled Exception:System.NullReferenceException: Object reference not set to an instance of an object.
One of the most annoying things about the NullReferenceException is that it is so vague. It never actually specifies which reference, exactly, was not set to an instance of an object. This can make debugging, or reporting problems, much more difficult. In the above example, we’re not specifically throwing any exception, but we are allowing a NullReferenceException to be thrown in the event that we’re unsuccessful in looking up a Subject for a given ID. It’s still a part of our design to rely on NullReferenceException, though in this case it’s implicit. What if instead of returning null from GetSubject we threw a SubjectNotFoundException? Or if we weren’t sure that an exception made sense in every scenario, what if we checked for null and then threw a better exception before moving on to work with the returned subject, like in this example:
var subject = GetSubject(subjectId);if (subject == null) throw new SubjectNotFoundException(subjectId);subject.DoSomething();
If we don’t follow this approach, and instead we let the NullReference propagate up the stack, it’s likely (if the application doesn’t simply show a Yellow Screen of Death or a default Oops page) that we will try to catch NullReferenceException and inform the user of what might be the problem. But by then we might be so far removed from the exception that even we can’t know for sure what might have been null and resulted in the exception being thrown. It's also possible that this exception might be thrown in the middle of a long multi-line LINQ statement or object initializer, making it difficult to know what, exactly, was null. Raising a more specific, higher level exception makes our own exception handlers much easier to write.
As I described earlier, it's very easy to write a custom exception for the case where no Subject exists for a given Subject ID. You should name it something very specific, and end the name with the Exception
suffix. In this case, we're going to call it SubjectDoesNotExistException
(or maybe SubjectNotFoundException
), since that seems very clear to me. You can create a class that inherits from Exception
and use constructor chaining to pass in some information to the base Exception
constructor, like this:
public class SubjectDoesNotExistException : Exception{ public SubjectDoesNotExistException(int subjectId) : base($"Subject with ID \"{subjectId}\" does not exist.") { }}
(you'll find code samples in the show notes for www.weeklydevtips.com/007)
Now in the example above, with no error handling in place, the user will get a message stating "Subject with ID 123 does not exist." instead of "Object reference not set to an instance of an object." which is far more useful for debugging or reporting purposes. In general, you should avoid putting custom logic into your custom exceptions. In most scenarios, custom exceptions should consist only of a class definition and one or more constructors that chain to the base Exception
constructor.
If you follow domain-driven design, I recommend placing most of your business-logic related exceptions in your Core project, within your domain model. You should be able to easily unit test that these exceptions are thrown when you expect them to be from your entities and services.