Mrinal Kamboj
Mrinal Kamboj

Reputation: 11480

Exception design - Custom exception at every layer

In our system design we have following layers:

Web API -> BusinessLayer -> HelperLayer -> DataLayer - Call hierarchy

Web API is the Rest service layer, Business does business operation on the business entity, Helper does the transformation of Data entity to Business Entity and Data fetch the POCO from database

As we are discussing the exception management strategy of the system, following are the two views:

I prefer that all errors shall propagate till the Web API, where we use Error filter to intercept, log the errors and change Context.Response to provide a friendly message to the end user, benefits of the same is:

  1. Error source remains intact

  2. We handle the exception where its required

  3. Simple and straightforward mechanism for error handling

What another set of team mates prefer is we create a custom exception for each layer, like DALException, HelperException, BusinessException, where a given layer throws the Exception, calling layer handles it, fills the inner exception and thus continue, as per them the benefit is:

  1. Each layer can provide a custom information of the issue / exception, which would help in error / exception abstraction

For me the the issue with this design is:

  1. Changing the source of exception which is nopt a good practice
  2. Catching exception without any processing
  3. Lot of extra code by adding try catch everywhere, which can impact performance in my understanding

Only benefit I see is we can provide specific message to the customer, but that is even possible, if we understand the underlying core exception and differentiate based on some code and thus provide a custom message like ABC failed instead of a generic message.

Please share your view and let me know if a clarification is required

Upvotes: 3

Views: 3112

Answers (3)

Scott Hannen
Scott Hannen

Reputation: 29202

I suggest avoiding the question (somewhat) by using interceptors rather than placing any of that logic directly in your classes and methods.

If the responsibility of a class is to receive a request for some data and return it from SQL then it shouldn't be concerned with which types of exceptions which layers expect. That exception handling becomes additional logic which is outside of that class's responsibility.

There are a number of different ways to implement interception. It might depend on what tools are already incorporated in your applications. I use Windsor for dependency injection so it's convenient to use their interceptors. If I wasn't using Windsor then I'd look at PostSharp.

But the net effect is that you either have an attribute on your class or a declaration when you declare your dependencies, and then all the logic of "this type of exception was thrown, catch it, wrap it and that, and re-throw" all lives in an interceptor class. You can change it back and forth without polluting your other classes.

99% of the time this enables me to have no try/catch blocks in my code at all. Logging and rethrowing is relegated to the interceptors. The only time I have any exception handling is if I need to handle something gracefully, so I need to catch an exception, log it, and return a non-exception result.


Unrelated to interception:

In practice, I've found that most of the time having one type of exception vs. another or wrapping exceptions on other types is useless. 99% of the battle is just having the exception detail vs having nothing. As long as no one does throw ex (obliterating the stack trace) then you'll have what you need in the exception detail. If we get clever with wrapping exceptions in more exceptions then we just create more information that we're going to ignore. We'll have more detail to sift through as we look for the one thing we actually care about - what was the exception and where was it thrown?

The only exception (I really wish I had a synonym for that) is if the business layer throws an exception containing user-facing information. For example, the user tries to update something, they can't, and you want to wrap the exception so that it explains what they need to correct. The specific type of exception could indicate that the exception is user-facing.

But if the exception message is the result of business logic ("You can't place this order because the item is out of stock") then is that really an exception at all? Perhaps that call should return a failure message rather than throwing an exception. We shouldn't use exceptions to communicate messages.

Upvotes: 2

Ivan Golovach
Ivan Golovach

Reputation: 209

What about rethrowing exceptions with exception chaining and parse this chain in Filter (select only first exception in chain (cause) or parse all contexts for all layers)?

class App {
    public static void main(String[] args) {
        View view = new View();
        try {
            System.out.println(view.doRender());
        } catch (ViewException e) {
            System.out.println("ERROR: " + unrollChain(e));
        }
    }

    static String unrollChain(Exception e) {
        Throwable current = e;
        while (current.getCause() != null) {
            current = current.getCause();
        }
        return current.getMessage();
    }
}

class View {
    private Business business = new Business();

    String doRender() throws ViewException {
        try {
            return "<html>" + business.doBusiness() + "</html>";
        } catch (BusinessException e) {
            if (System.nanoTime() % 2 == 0)
                throw new ViewException("Some context if have one", e);
            else
                throw new ViewException(e); // no local context, pure rethrow
        }
    }
}

class Business {
    private Dao dao = new Dao();

    int doBusiness() throws BusinessException {
        try {
            return dao.select() + 42;
        } catch (DaoException e) {
            if (System.nanoTime() % 2 == 0)
                throw new BusinessException("Some context if have one", e);
            else
                throw new BusinessException(e); // no local context, pure rethrow
        }
    }
}

class Dao {
    int select() throws DaoException {
        if (System.nanoTime() % 2 == 0)
            return 42;
        else
            throw new DaoException();
    }
}

class DaoException extends Exception {
}

class BusinessException extends Exception {
    public BusinessException(String message, Throwable cause) {
        super(message, cause);
    }

    public BusinessException(Throwable cause) {
        super(cause);
    }
}

class ViewException extends Exception {
    public ViewException(String message, Throwable cause) {
        super(message, cause);
    }    
    public ViewException(Throwable cause) {
        super(cause);
    }
}

Upvotes: 1

Paul Cat
Paul Cat

Reputation: 11

It is valuable to have the stack trac of where the error was initally thrown. If exceptions are caught and rethrown it is important that the exception is added to the inner exception so that the information is not lost. In my experience the argument for catching and rethrowing exceptions comes from developers not familiar with debugging by using the stack trace.

If you are catching and rethrowing exceptions it does create extra code to itterate through inner exceptions. This data is has to be logged or transmitted, which increases storage requirements or network overheads. There is also then more data to sift through when it comes to debugging issues related to the exceptions thrown which can increase the amount of deeveloper time required.

On the other hand if a developer can predict an exception and use this insight at the time of writing the code it can be helpful to provide extra top level information so in this case it can be valid to catch and rethrow the exception. Especially where other (potentially less experienced) developers will be working on the same code. If the exception is predictable however in many cases it can be better to handle the error and behave accordingly rather than erthrow the exception.

It can be valuable to catch exceptions at application/dll boundaraies. Doing so can make developing using these modules less brittle when those components are used by other developers or consumed by the end users.

Upvotes: 1

Related Questions