Exception Handling – How to Simplify for Library Users?

api-designcexceptionslibraries

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 the InnerException 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

Best Answer

You might be overthinking this.

You only need actual exception types if you want your caller to be able to differentiate at runtime, which action to take in response to the error.

The question is, can your caller do anything about the errors? Is one of them an error where the caller program could decide to do something about it? For example, lets say the endpoint is not available, is it possible and a plausible scenario that the caller says "oh, then take this other backup endpoint, that should work, try again please".

You said you have no business constraints that would let this fail, so it's basically down to infrastructure failures. In my experience, programs do not have a list of backup databases, alternative REST endpoints or similar. If such a thing exists, it is baked into the infrastructure as load balancers, server farms or something like that, it's not programmed into every single application. The program, when infrastructure fails, cannot do anything to fix that. It can report the failure and either die or move on. But it cannot correct it.

So based on that premise, your caller being unable to fix the mistake on the fly, there is no point in having different exceptions. Your call worked, or it did not. If it did not, the exception should contain enough information for humans to fix the problem when they read the logs. Take any exception type that fits, maybe create one new one for your framework if you think it's needed. But that's it.

Related Topic