DEV Community

Weekly Dev Tips

Prefer Custom Exceptions

Prefer Custom Exceptions

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.

Sponsor - DevIQ

Thanks to DevIQ for sponsoring this episode! Check out their list of available courses and how-to videos.

Show Notes / Transcript

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.

Throwing Low-Level Exceptions

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.

Replace Framework Exceptions with Custom Exceptions

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.

Writing Custom Exceptions

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.

Show Resources and Links

Episode source