28

I'm currently refactoring a large subsystem with a multi layered architecture, and I'm struggling to design an effective error logging \ handling strategy.

Let's say that my architecture consists of the following three layers:

  • Public Interface (I.E an MVC Controller)
  • Domain Layer
  • Data Access Layer

My source of confusion is where I should implement the error logging \ handling:

  1. The easiest solution would be to implement the logging at the top level (I.E the Public Interface \ MVC Controller). However this feels wrong because it means bubbling up the exception through the different layers, and then logging it; rather than logging the exception at it's source.

  2. Logging the exception at it's source is obviously the best solution because I have the most information. My problem with this is that I can't catch every exception at source without catching ALL exceptions, and in the domain / public interface layer, this will lead to catching exceptions that have already been caught, logged and re-thrown by the layer below.

  3. Another possible strategy is a mix of #1 and #2; whereby I catch specific exceptions at the layer they are most likely to be thrown (I.E Catching, logging and re-throwing SqlExceptions in the Data Access Layer) and then log any further uncaught exceptions at the top level. However this would also require me to catch and relog every exception at the top level, because I can't distinguish between errors that have already been logged \ handled vs those that have not.

Now, obviously this is an issue in most software applications, so there must be a standard solution to this problem that results in exceptions being caught at source, and logged once; however I just can't see how to do this myself.

Note, the title of this question is very similar to 'Logging exceptions in a multi tier application"', however the answers in that post are lacking detail and are not sufficient to answer my question.

4
  • 1
    One approach that I sometimes use is to create my own Exception (or RuntimeException) subclass and throw it, including the original exception as a nested one. Of course it means that when capturing exceptions at a upper levels I need to check the type of the exception (my own exceptions just get rethrown, other exceptions are logged and included into new instances of my exceptions). But I have been working solo for a long time so I can't give "official" advice.
    – SJuan76
    Commented Oct 22, 2017 at 20:34
  • 4
    The easiest solution would be to implement the logging at the top level - do this. Logging exceptions at their source isn't a good idea and every application I've encountered that does this was a PITA to debug. It should be the responsibility of the caller to handle exceptions.
    – Justin
    Commented Oct 22, 2017 at 21:17
  • You seem to have some specific implementation technique/language in mind, otherwise your statement "exceptions which were logged are undistinguishable from ones that weren't" is hard to understand. Can you give more context? Commented Oct 23, 2017 at 9:37
  • @Vroomfondel - You're right. I'm using C#, and wrapping the code in each Layer with try{ ... } catch(Exception ex) { Log(ex); } would result in the same exception being logged at each layer. (It also seems like rather bad practice to be catching every exception at every layer in the code base.)
    – KidCode
    Commented Oct 23, 2017 at 10:27

2 Answers 2

30

To your questions:

The easiest solution would be to implement the logging at the top level

Having the exception bubble up to the top level is an absolutely correct and plausible approach. None of the higher layer methods tries to continue some process after the failure, which typically can't succeed. And a well-equipped exception contains all the information necessary for logging. And doing nothing about exceptions helps you keep your code clean and focussed on the main task instead of the failures.

Logging the exception at it's source is obviously the best solution because I have the most information.

That's half correct. Yes, most useful information is available there. But I'd recommend to put all this into the exception object (if it isn't already there) instead of immediately logging it. If you log at a low level, you still need to throw an exception up to tell your callers that you didn't complete your job. This ends up in multiple logs of the same event.

Exceptions

My main guideline is to catch and log exceptions at the top level only. And all the layers below should make sure that all necessary failure information gets transported up to the top level. Within a single-process application e.g. in Java, this mostly means not to try/catch or log at all outside the top level.

Sometimes, you want some context information included in the exception log that's not available in the original exception, e.g. the SQL statement and parameters that were executed when the exception was thrown. Then you can catch the original exception and re-throw a new one, containing the original one plus the context.

Of course, real life sometimes interferes:

  • In Java, sometimes you have to catch an exception and wrap it into a different exception type just to obey some fixed method signatures. But if you re-throw an exception, make sure the re-thrown one contains all information needed for later logging.

  • If you are crossing an inter-process border, often you technically can't transfer the full exception object including the stack trace. And of course the connection might get lost. So here's a point where a service should log exceptions and then try its best to transmit as much of the failure information as possible across the line to its client. The service must make sure the client gets a failure notice, either by receiving a failure response or by running into a timeout in case of a broken connection. This will typically result in the same failure to be logged twice, once inside the service (with more detail) and once in the client's top level.

Logging

I'm adding some sentences about logging in general, not only exception-logging.

Besides exceptional situations, you want important activities of your application to get recorded in the log as well. So, use a logging framework.

Be careful about log levels (reading logs where debug information and serious errors are not flagged with different accordingly is a pain!). Typical log levels are:

  • ERROR: Some function failed irrecoverably. That doesn't necessarily mean your whole program crashed, but some task couldn't be completed. Typically, you have an exception object describing the failure.
  • WARNING: Something strange happened, but didn't cause any task to fail (strange configuration detected, temporary connection breakdown causing some retries, etc.)
  • INFO: You want to communicate some significant program action to the local system administrator (starting some service with its configuration and software version, importing data files into the database, users logging into the system, you get the idea...).
  • DEBUG: Things you as the developer want to see when you are debugging some issue (but you'll never know in advance what you really need in case of this or that specific bug - if you can foresee it, you'll fix the bug). One thing that's always useful is to log activities on external interfaces.

In production, set the log level to INFO. The results should be useful to a system administrator so he knows what's going on. Expect him to call you for assistance or bug-fixing for every ERROR in the log and half of the WARNINGs.

Enable the DEBUG level only during real debugging sessions.

Group the log entries into appropriate categories (e.g. by the fully-qualified classname of the code that generates the entry), allowing you to switch on debug logs for specific parts of your program.

2
  • Thanks for such a detailed answer, I really appreciate the logging part too.
    – KidCode
    Commented Oct 23, 2017 at 19:58
  • So if you only implement try/catch at the top level then do you implement retry at the top level as well since that is where the exception bubbles up to? I could see try/catch/retry handled at a low level as well since the top level doesn't need to know about retrying if it succeeds.
    – alex
    Commented Oct 26, 2020 at 16:02
0

I'm bracing myself for the downvotes, but I'm going to go out on a limb and say I'm not sure I can agree with this.

Bubbling exceptions up, much less logging them again, is extra effort with little benefit. Catch the exception at source (yes, easiest), log it, but then don't re-throw the exception, just report "error" to the caller. "-1", null, empty string, some enum, whatever. The caller only needs to know that the call failed, almost never the gruesome details. And those will be in your log, right? In the rare case that the caller does need the details, go ahead and bubble up, but not as an automatic unthinking default.

2
  • 4
    The problem with reporting the error by return values is: 1. If the caller cares about failures it has to check the special value. But wait, is it null or the empty string? Is it -1 or any negative number? 2. If the caller does not care (i.e. does not check) this leads to follow up errors unrelated to the original cause, e.g. a NullPointerException. Or worse: The calculation continues with wrong values. 3. If the caller would care but the programmer does not think of this method failing, the compiler does not remind him. Exceptions don't have this problems, either you catch or you rethrow.
    – siegi
    Commented Apr 27, 2018 at 6:21
  • 2
    Sorry, I had to downvote that. The approach you describe (replace exceptions with special return values) was state-of-the-art in the 1970s, when the C language originated, and there are many good reasons why modern languages have exceptions, and to me the main one is that using exceptions properly makes it much easier to write robust code. And your "Bubbling exceptions up [...] is extra effort [...]" is plain wrong: just do nothing about exceptions, and they will bubble up all by themselves. Commented Apr 27, 2018 at 8:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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