Suppose of having a library exposing the following piece of functionality:
public static class AwesomeHelpers
{
public static async Task<int> ComputeSomethingImportAsync(CalculationInputs input)
{
int result = 13;
// actual implementation is not relevant for this discussion
return result;
}
}
Suppose also that the calculation could fail for 3 different reasons, depending on external services used by the calculation algorithm:
- an external API returns a non success status code
- a record of data is missing in the database
- another external API returns a response containing invalid JSON (response content cannot be parsed as JSON)
The public API of the library either returns an integer which is the calculation result or throws a proper exception to communicate the calculation failure to the library users. The possible failures of the calculation process are only the ones listed above. Notice that there is not the possibility of a business failure of the calculation (e.g.: the calculation process cannot be completed based on a certain business rule); the only possible failures depend on errors at the level of external services, so throwing an exception to the library user seems the correct thing to do here.
Given this scenario, there are 2 main strategies I can think of:
- throw 3 different exception types, each one corresponding to one of the possible failures (e.g.:
FooApiErrorException
,MissingRecordException
,BarApiErrorException
) - define one custom exception type to be used each time something goes wrong with the calculation process (e.g.:
CalculationProcessException
). In this case both the exception message and theInnerException
property can be used to provide further details on the error.
In both cases the exceptions raised by the library need to be carefully documented (e.g.: XML documentation for Visual Studio intellisense).
Based on my experience and personal preference, the second strategy (defining one custom exception type to be used for all possible failures and specifying the error details with the message and the InnerException
property) is the best solution, because it allows the library user to write simpler code.
What I mean is that this code
try
{
result = await AwesomeHelpers.ComputeSomethingImportAsync(input);
}
catch(CalculationProcessException exception)
{
// handle the error
// the original exception is inside the InnerException property
// the error message explains what happened
}
is simpler than this:
try
{
result = await AwesomeHelpers.ComputeSomethingImportAsync(input);
}
catch(FooApiErrorException exception)
{
// handle Foo API error
}
catch(MissingRecordException exception)
{
// handle missing record error
}
catch(BarApiErrorException exception)
{
// handle Bar API error
}
What do you think about this ? Any thoughts or observation on this ? Do you know any useful reference for this code design issue ?
EDIT 31st Agust 2021
It's interesting to notice that the Microsoft Azure SDK for Service Bus has adopted the approach of defining just one exception type (called ServiceBusException
) for any error thrown by the library.
They use an enum property on the exception object (called Reason
) to allow the client code to understand the root cause of the error. The details are available here