1

I am currently thinking about some design choices regarding exception handling.
My current architecture looks a little like this:

enter image description here

You can see that I have a UI where a try-catch middleware is cathing exceptions like this:

Pseudocode:

catch(CoreUserNotFoundException e)
{
   return 'You provided an invalid user id'
}
catch(CoreToLongException e)
{
   return 'The name is too long'
}
catch(Exception e)
{
   logger.log(e)
   return 'Something went wrong'
}

So only some messages will be returned to the user. All other exceptions are just logged.

Lets say someone wants to add an entity with a user id that has to be validated.
But how do I know weather the user exists or not?
This is done with my business logic. It provides an interface "IUserService" to retrieve a user. The business logic does not care how the app is doing this.
It could get the user via Graph API of Microsoft, or via Auth0 or via Facebook or whatever.
My business logic should work with all kinds of user services.

So in my picture I have a MicrosoftGraph Adapter, that implements the IUserService. This adapter is using my own Graph SDK - this SDK uses the original SDK of Microsoft, but I like my own wrapper and use it in many apps.

In my current design the "MS Graph SDK" throws an Exception if a user does not exist for an id. This exception is bubbling through my own Graph SDK and caught in the Graph Adapter of my app.

The Graph adapter then transforms this Exception in a CoreUserNotFoundException - the one I am showing to the user:

Pseudocode:

   try {
      user = _userService.getUserById(userId);
   } catch(GraphUserNotFoundException e)
      throw new CoreUserNotFoundException("You provided a bad user id", e);
   }

Of course there are alternative ways:
I could also define an Exception in my Own Graph SDK and re-use this exception through all my projects.
But to show the user a nice message, my UI needed to have a dependency on "Own Graph SDK" because I want to do:

catch(MyOwnGraphSDKUserNotFoundException e)
{
   return 'You provided an invalid user id'
}
catch(Exception e)
{
   logger.log(e)
   return 'Something went wrong'
}

What is probably a bad practice?
In this case I would have to change the UI-code if the Adapter changes, so I am not doing it.

My questions to you:

Was my design choice correct? Or does it seem unprofessional to you? I am still learning about clean architectures, hexagonal architectures etc., but didnt read too much about Exception handling in this topic.

3
  • In my current design the "MS Graph SDK" throws an Exception if a user does not exist for an id. This exception is bubbling through my own Graph SDK and caught in the Graph Adapter of my app. hell. why? What's the point of the wrapper then?
    – Laiv
    May 31, 2021 at 19:54
  • The wrapper has other reasons - I am wrapping the MS Graph, because my own wrapper is doing some things with the requests - but this is unrelated to Exception handling: Those can bubble ;) Jun 1, 2021 at 6:59
  • Just for the record - if you return humanly readable strings, you will need to redo it when you need to support multiple languages. Jun 30, 2021 at 20:07

2 Answers 2

1

Your approach is the one deemed most correct. For the reasons you presented and because you do not want the user to receive a message directly from the 3rd party, that may not be correct for your users, in your business.

2
  • I do not have control over 3rd party exceptions. They might contain informatino that is a security issue and should not be exposed to the user. I thought its a general best practice to define only own exceptions and show only the message of those to the user, the own domain can handle. All 3rd party exceptions can go to log, not to the user. Or am I wrong? Jun 1, 2021 at 7:01
  • Yes, that's what I intended to convey. Your users should only see messages from you, that you control and know are relevant to your domain. Third party Exceptions should never reach your users. Jun 1, 2021 at 15:13
1

This is a good time to remember that an exception is exceptional control flow, and the purpose is to give the opportunity for a caller to recover from the exception. (Contrast with "errors" which are believed to be unrecoverable).

You should only catch an exception and handle it if you can provide the caller with the ability to recover. This means you need to understand the caught exception well enough to know what it means, and recover enough to throw your own exception or log a message. Then that exception-catching behavior is simply part of the specified behavior of the algorithm.

As an extreme example, if you try to catch some SeriousSecurityException, which states that all future calls into the API could result in vulnerabilities to exploit, and this can be resolved by calling .restart() on some underlying object. You can safely call .restart() and throw your own RequestFailedException. You handled the situation, and raised a new API.

On the other hand, consider if you aren't in a position to call .restart(). Maybe you don't have the pointer to the correct object. Now you have to be careful. If you throw RequestFailedException, and they try to handle it, they have no idea that they need to call restart() to return the system to a usable state.

At this point, you would have to transition to RequestFailedError. Something went wrong, and you have no reason to believe there is any way for the caller function to return to a usable state. There should be no attempt to recover. Or, if you know you're the only one using that particular security API, you might set a flag to throw a NoLongerValidException indicating that your whole API is no longer usable. That would be properly handling your exception, returning to a state that is recoverable (even if it can't do anything).

You should never ever ever ever catch Exception and keep going. You have no idea what went wrong, so you have no way of knowing whether the system can be recovered into a safe state. At the very least, you have to catch it and then throw your own Error to say "Something went wrong, and you no longer know anything about the state of your program."

As an interesting middle ground, several languages offer a concept of an "inner exception," which is the exception that caused the exception that was thrown. This makes it easy to wrap a lower-level exception in your own class. All you have to do is specify the semantics of your exception class enough, typically "Treat this as an error unless you can recognize the inner exception and do X Y and Z about it."

Your Answer

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

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