Failure is a subtype of Success
This article considers how to cleanly handle both the failure and success paths in code, taking a look at how Java's new pattern matching can help, discussing the limitations of subtyping along the way.
Introduction
It is well known that error-handling code is buggy [2, references 22, 44 and 55] so that a coding approach that helps with error paths should improve code quality.
Consider this getPage method that downloads a webpage using HTTP GET.
All being well, getPage retrieves the webpage (line 5), checks the HTTP status code to be in the 200-299 success range, before returning at line 11.
However, the retrieval is equally likely to fail. A few reasons are:
If an exception is raised, getPage will return null. In the code above, as written, there isn't much opportunity to recover if an exception occurs. Rather than have the calling code handle the exceptions, getPage catches them so that this method either returns the webpage or null.
In a previous article, I described why not returning null is a good thing [1].
In summary, software engineers return null to represent a fact in the problem domain, typically that some item cannot be found, in our case a webpage. But it is not clear why null has been returned. Is the URL to a page that does not exist or has something else happened? null does not communicate any useful information.
In addition, if the null reference case is not appropriately handled once getPage returns, a NullPointerException becomes possible. And even if such code addresses the null reference case, no remediation can be performed, other than to log that null was passed back for a given URL.
In fact, the code above throws away useful information. At the if-statement on line 10, if the HTTP status code is not in the success range of 200-299, null is returned, throwing away all status codes less than 200 and greater than 300 which is unfortunate as the HTTP 400 range indicates client errors with those above 500 informing us about server errors [4]. It would be useful to return the status code to the calling code. However, getPage returns String so there is no opportunity to return an integer (apart from encoding it as a String which is not recommended as the calling code would have to distinguish between a valid HTML response and an encoded status value, purely by inspecting the string contents. Such code is likely to be both harder to maintain and a source of subtle bugs.)
As the method returns String, null is returned to indicate something other than a webpage. null is returned as it is type compatible with a String.
The above code fails in one of two ways, either the GET failed because a non-success value was passed back, or an exception was raised. Both code paths above ignore this distinction when passing back null. Ignoring errors and allowing them to propagate (in this case, disguised as a null) is bad programming practise [2, references 7 and 60].
This article describes how the two cases of success and failure can be cleanly handled, successfully capturing the context of the failure and ensuring logging is consistent.
The Success<T> and Failure <T> Types
In the above code I want to return non-null values that capture the three outcomes of the method: one of a webpage, an HTTP error value, or an exception. I prefer to return these values as the control flow is easier to reason about in both success and failure conditions.
Returning Success
Success<T> wraps a value of type T at construction time, passing it back from the unwrap method. The other methods are described below.
getPage now returns a Success<String> and it is used like this:
Returning Failure
Failure<T> is defined to be:
The eval method returns true on Success and false on Failure.
This lets us do this:
We can now distinguish between success and failure and no null value is returned.
Updating getPage
The getPage method is rewritten like this:
The two different ways of failing are now abstracted into FailureValue and FailureException which are defined like this respectively:
In your code, you could add logging code in both constructors so that your logging is consistent across projects and so your engineers can focus on higher value tasks, rather than figuring out what strings to write.
Both FailureValue<T> and FailureException<T> are subclasses of Failure<T>. This is to ensure subtype compatibility so instances of the classes that model failure can be passed back from getPage that is typed from Success<T>. This is the reason failure is a subtype of success.
领英推荐
Calling getPage
The call to getPage now becomes:
When called above with the URL https://www.example.com/nosuchpage, the getPage method returns at line 13, as can be seen below:
Distinguishing Failure
When calling getPage as below:
If the GET is successful, a Success<String> is passed back and eval will return true, letting the calling code handle the success path.
If an HTTP error code is returned a FailureValue<String> is returned. If getPage handles an exception, a FailureException<String> is returned.
As both failure classes subclass Failure<T>, eval will return false.
Handling Failure
The code in calling getPage above fails to retrieve anything as there is no resource at https://www.example.com/nosuchpage. Therefore, a FailureValue<String>(404) is created. page.eval returns false so the else executes and failed is assigned (see Open Questions below).
We distinguish between the two kinds of failure using a Java pattern match [3].
Line 6 switches on the run-time class of failed (page is equivalent). When the code above is run failed will be an instance of FailureValue<String> so line 7 will match.
As currently written, in the failure case failed will only be an instance of FailureValue or FailureException, therefore, the default will never run. However, this could change in future if a third failure type was introduced and the switch above was not updated to accommodate it.
Limits of Subtyping
This example demonstrates a limitation of subtyping.
In the failure case, a Java Number such as 404 or an exception is returned.
FailureValue wraps the java.lang.Number. FailureException wraps java.lang.Exception. Neither of these classes share a useful superclass so that only a single failure class would be required.
However, code that fails does so in mutually different ways. Sometimes this can be conveniently represented using a number. Sometimes, an exception is required.
This is the reason that there are multiple subclasses of Failure<T>.
We can imagine a FailureSignal<T> that would represent a failure in code due to an operating system signal being received.
As we have more than one subclass and want to call methods that return different types, no single failure class can model this, and the pattern match above is a convenient way to handle this.
Putting it all Together
The code below calls getPage three times, to demonstrate how the three cases are handled:
On running the above, you will see:
Which is to be expected. The success and failure cases are cleanly separated and the two kinds of failure can be distinguished, and no null checks are required.
Open Questions
The call that assigns failed on line 5 in calling getPage is redundant as page can be passed to the switch-statement on line 6. However, conceptually, it's useful as it's a reminder to the software engineer that retrieval of the page has failed and that processing is stepping over to failure handling.
Acknowledgements
Thanks to Dave Farley for the reference to [2].
Resources
[1] https://www.dhirubhai.net/pulse/returning-null-considered-harmful-huw-evans/
[2] https://www.usenix.org/system/files/conference/osdi14/osdi14-paper-yuan.pdf
[3] https://docs.oracle.com/en/java/javase/17/language/pattern-matching.html#GUID-A59EF0C7-4CB7-4555-986D-0FD804555C25
[4] https://developer.mozilla.org/en-US/docs/Web/HTTP/Status