Suppose we’re writing code that calls some external API, and this external API returns a JSON payload. When we call this API, there’s some nonzero chance that it will return invalid JSON (ie. a malformed response).1 In the case when we receive a malformed response, we want to raise an exception back up the call stack. The code may look something like so:
Suppose that we run this code in production, and while it works for the happy path where SomeAPIClient
returns valid JSON, we find some issue with the external API we’re unable to fix. For example, we may find that due to a bug in the API endpoint that SomeAPIClient
calls, SomeAPIClient
returns a string called "bar"
(ie. a malformed JSON payload) for some fraction of the calls where parameters_hash
has a key of :foo
.
In this case, we treat this error as a known issue that we’re unable to solve, and so we want to write a workaround to gracefully handle this error. To write a workaround, we need error-handling logic that’s more specific than the default behaviour of raising a JSON::ParserError
. We might be tempted to write code that looks like this2:
By raising a InvalidResponseForFoo
when we see the malformed response of "bar"
, the above code handles the known issue with more specificity than simply raising a JSON::ParserError
.3 However, the above code is insufficiently specific, and it may mask unrelated bugs that are introduced into our system in the future.
For example, what would happen if SomeAPIClient
started returning "bar"
intermittently for every value of parameters_hash
, not just requests containing a key of :foo
? Our application would raise InvalidResponseForFoo
errors for requests completely unrelated to the key of :foo
, meaning our workaround is executed for a use case it was not designed for.
In this example, a better way to handle the error would be like so:
In the revised error handling code above, we explicitly check if the key :foo
exists in our input parameter. This ensures that we’re only raising a InvalidResponseForFoo
for the known issue we originally observed.
This example is specific to JSON, but there’s a generalized takeaway from this: if we’re writing workaround code to deal with a known issue, we should be as specific as possible when determining whether or not to execute that workaround. This prevents us from masking future bugs that may look similar to to our known issue, but could be a different issue altogether.
Start your journey towards writing better software, and watch this space for new content.
1: Hint: we should assume this is true for every API call that we make!
2: In this example, we raise a custom error to handle this known issue. However, this isn’t the only way to handle a known issue. For example, we may want to return nil
or implement some use-case specific logic.
3: A more efficient approach would be to write raise InvalidResponseForFoo if raw_response == 'bar'
before we call JSON.parse(raw_response)
. This would allow us to only raise one error, and avoid writing an explicit rescue block for JSON::ParserError
. However, I found the example to be a bit more readable with the error handling code written at the end of the method definition.