0

Given that when is supported in try...catch blocks, does it make sense to create a single CustomApplicationException class, say, with an enum ApplicationExceptionReason Reason property to differentiate "reasons" instead of creating a new exception type for each reason (it could have Flags, if multiple reasons are necessary/possible)?

3
  • 1
    Having a single custom exception for an entire application seems like a bad idea. HTTP response codes are a good example of variants of the same exception. I certainly wouldn't combine a FileNotFoundException and DatabaseConnectionFailedException.
    – Dan Wilson
    May 29 '19 at 15:42
  • @DanWilson So, FileNotFoundException and PostgresException already exist... I'm not talking about wrapping all other exceptions in a custom ApplicationException, but using it for domain specific problems that aren't otherwise covered. May 29 '19 at 20:45
  • Actually, the PostgresException's SqlState property seems like a perfectly reasonable example of this, except in the domain of database operations and in the context of database server. Seems like a point in favor? May 29 '19 at 20:48
1

There are three separate issues at play; and in order to answer your question, I need to address all three issues.

  1. Custom Application Exceptions should be for exceptional events, and not errors that are not exceptional events. Think of an invalid user input. Users getting input wrong is not exceptional; and shouldn't be treated as such. A hostile user trying to log in to an application is not an exceptional activity; it's normal and should be treated as such.

  2. As a corollary to this, Errors from the domain are by definition not exceptional events, and therefore should not be handled with exceptions.

  3. Much like how the OSI model handles segregating the different factors at play to deliver content , errors exist at the very top of the stack and therefore should be unaware of the mechanisms below them.

So the first question to ask:

  1. Are these domain errors truly exceptional events? Are they instances where invariants or quasi-invariants are violated? A good way to think about whether that's true is whether you can demonstrate that a Debug.Assert is a good addition to that particular code path.

  2. If they are truly exceptional events, and they're domain 'errors', Is the application written in the fashion of Domain Driven Design? That is, does the application use bounded contexts and the language of the domain in its architecture? If so, you have a good architectural reason to drive for custom Application Exceptions, otherwise perhaps look at a different approach.

  3. Do you mind the coupling you're going to be putting in to every class or operation you create going forward?

If we take your design to its logical conclusion, I can imagine a caller and callee looking like the following:

/// <summary>Does Domain Operation</summary>
/// <exception>ApplicationException</exception>
/// <comments>Throws Reason.NotValid when a user does not provide a valid domain operations. Throws Reason.CantCompute when the user provides a domain question we can't answer</comments>
public string SomeDomainOperation(string q)
{
  if (!Valid(q)) {
    throw new ApplicationExcpetion(Reason.NotValid);
  }
  if (!Compute(q)) {
    throw new ApplicationException(Reason.CantCompute);
  }
}

public static void Main(strings[] args) {
  try {
    SomeDomainOperation("hi");
  }
  catch (ApplicationException ex) when (ex.Reason == Reason.NotValid) {
    Console.Writeline("Not Valid");
  }
  catch (ApplicationException ex) when (ex.Reason == Reason.CantCompute) {
    Console.Writeline("Can't compute");
  } 
}

You now have three issues maintenance programmers must contend with.

  1. You now have every new error as an enum. Unfortunately Enums don't provide extra information. For instance, if you want to have an error message with each enum reason, you now need a Custom Exception anyway. Or, you quickly have to make that Enum an actual class, at which point you should make it a custom exception anyway.

  2. Each caller must now specific more information for when it should catch; this is a small concern but every single catch operation will now take up more horizontal space. Contra the above with :

  catch (ApplicationException ex) when (ex.Reason == Reason.CantCompute) {
    Console.Writeline("Can't compute");
  } 
  catch (CantComputeException ex)  {
    Console.WriteLine("Can't compute");
  }

  1. It is now easier to confuse domain exceptions with domain errors that aren't exceptional, and harder for the maintenance programmer to understand why this is in place.

I'll use PostgresException's SqlState as an example. Not all of those events are exceptional, and most likely should be handled in other ways. OutOfMemory seems exceptional; but a network error is normal, unfortunately.

Finally, I'll leave you with Microsoft's own guidance on this matter:

For conditions that are likely to occur but might trigger an exception, consider handling them in a way that will avoid the exception.

and:

The method to choose depends on how often you expect the event to occur.

Use exception handling if the event doesn't occur very often, that is, if the event is truly exceptional and indicates an error (such as an unexpected end-of-file). When you use exception handling, less code is executed in normal conditions.

Check for error conditions in code if the event happens routinely and could be considered part of normal execution. When you check for common error conditions, less code is executed because you avoid exceptions.

And:

Design classes so that exceptions can be avoided

A class can provide methods or properties that enable you to avoid making a call that would trigger an exception. For example, a FileStream class provides methods that help determine whether the end of the file has been reached. These can be used to avoid the exception that is thrown if you read past the end of the file. The following example shows how to read to the end of a file without triggering an exception.

Another way to avoid exceptions is to return null (or default) for extremely common error cases instead of throwing an exception. An extremely common error case can be considered normal flow of control. By returning null (or default) in these cases, you minimize the performance impact to an app.

But why does Microsoft say this? Because throwing exceptions has a huge performance hit on applications!

Bottom line:

  1. Use Errors and Error Codes (probably as custom classes) if the domain conditions are errors but not exceptional events.

  2. Use Exceptions for exceptional events; if they start to become flow control in your application, that's a sign they are no longer exceptional events.

  3. Think of how the OSI model operates and don't mix the layers. All exceptions are errors but not all errors are exceptions. An error that is not an exception shouldn't have to ride with an exception to be useful.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.