I've always been taught that fatal exceptions (indicating problems that cannot be solved programmaticaly) and boneheaded exceptions (resulting from bugs in my code) should not be caught, should not be handled, should not be ignored, instead they should be allowed to crash the app. The reasoning is that once such an exception is thrown the state of the application is undefined so we need to stop any further damage that might result from keeping our app running. Also bugs should be fixed and crashing the app forces us to do so ASAP; if the app ignored the exception instead then the bug could remain unnoticed and unfixed.
Boneheaded exceptions are your own darn fault, you could have prevented them and therefore they are bugs in your code. You should not catch them; doing so is hiding a bug in your code. Rather, you should write your code so that the exception cannot possibly happen in the first place, and therefore does not need to be caught. That argument is null, that typecast is bad, that index is out of range, you’re trying to divide by zero – these are all problems that you could have prevented very easily in the first place, so prevent the mess in the first place rather than trying to clean it up.
(https://ericlippert.com/2008/09/10/vexing-exceptions/)
However, there are cases where crashing the application seems unacceptable.
Let me provide an example. We need to synchronize data between an e-commerce website and an ERP system. To this end we have a service that periodically downloads new orders from the e-commerce platform and inserterts them into the ERP system. Both e-commerce shop and ERP are 3rd party solutions that are outside of our control.
This process can fail for numerous reasons:
- The ERP system is a poorly documented mess. It can reject any data that fails its validation rules (such as: surname too long) and unfortunately its validation rules are not immediately clear. I also saw cases where calling setters exposed by the public API of the ERP in the wrong order produces exceptions. As well as a plethora of other not immediately obvious cases where ERP might throw.
- The e-commerce system returns orders serialized to JSON. These JSONs will have to be deserialized by our app. Unfortunately the exact fields of the JSON may vary slightly depending on the details of the order and it does sometimes happen that the deserialization of a particular order fails because we did not foresee some particular condition.
This does not invalidate the general rule that boneheaded exceptions should not happen in the first place, but it does make it rather hard to ensure that no boneheaded exceptions may ever happen.
But now requirements of high fault tolerance come. It is unacceptable to fail to transfer all orders because a single order could not be transferred.
This leads to following pattern:
for(var order in ordersToTranfer)
{
try
{
WriteToERP(order);
}
catch(Exception e) // any exception
{
LogException(e);
AddToListOfErrorsThatRequireRetryingAndPossiblyManualIntervention(order);
continue;
}
}
This violates the general rule that boneheaded exceptions must not be caught.
Is there any way to have the cake and eat it too? To avoid catching all exceptions while still stopping the failure of transferring a single order from crashing the whole application?
All that comes to my mind is to move from a monolith to microservices. Service A downloads orders from e-commerce website but does not deserialize the JSON except insofar as necessary to split the list of orders into individual orders. Then it invokes an enpoint exposed by service B once for every order. Service B deserializes each received order and passes it to service C (again invoking an endpoint of service C once for each received order) which saves the order to the ERP. If the services are implemented as, for example, stateless HTTP servers that expose a REST API then fault tolerance will be provided by the environment? A typical server does not stop processing other requests because one request faulted; instead it returns HTTP 500 from that particular request and handles other requests without interruption? Solutions such as IIS or Apache enforce this.
- Is the requirement of high fault tolerance enough of a reason to pay the non-trivial costs of increased complexity that come with the microservice architecture?
- If yes, then what are the exact advantages of doing so as opposed to manually catching all exceptions?
- If no, then what to do instead?