Failure is a subtype of Success
Image (C) HR Daily Advisor: https://hrdailyadvisor.blr.com/2019/04/08/4-consequences-of-failing-to-embrace-failure/

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.

No alt text provided for this image

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:

  1. The URL requested (line 1) may be malformed
  2. While the GET is being executed (line 5) network access may fail
  3. The remote service encounters a fault while satisfying the GET request

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

No alt text provided for this image

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:

No alt text provided for this image

Returning Failure

Failure<T> is defined to be:

No alt text provided for this image

The eval method returns true on Success and false on Failure.

This lets us do this:

No alt text provided for this image

We can now distinguish between success and failure and no null value is returned.

Updating getPage

The getPage method is rewritten like this:

No alt text provided for this image

The two different ways of failing are now abstracted into FailureValue and FailureException which are defined like this respectively:

No alt text provided for this image
No alt text provided for this image

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:

No alt text provided for this image

When called above with the URL https://www.example.com/nosuchpage, the getPage method returns at line 13, as can be seen below:

No alt text provided for this image

Distinguishing Failure

When calling getPage as below:

No alt text provided for this image
calling getPage

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.

No alt text provided for this image

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:

No alt text provided for this image

On running the above, you will see:

No alt text provided for this image

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

要查看或添加评论,请登录

Huw Evans的更多文章

  • The Joys of Caching

    The Joys of Caching

    Caching data can improve system performance. Let's take a look.

  • Debugging and the Scientific Method

    Debugging and the Scientific Method

    The scientific method helps you gain knowledge [1]. You make an observation and test it with an experiment that shows…

  • Debugging

    Debugging

    This is typically how I go about debugging a piece of code: What is wrong? Reproducing the error Finding the source of…

  • Understanding Inconsistency with SUDs

    Understanding Inconsistency with SUDs

    This article shows why inconsistency and latency are fundamental when building distributed systems and how PACELC and…

  • Software Engineering builds two Things

    Software Engineering builds two Things

    When we write software, we build two things. The software that provides the business solution.

  • Smaller teams are more reactive

    Smaller teams are more reactive

    On November 2 2022, I wrote an article on how I had recruited 12 new employees. This article covers what happened next.

  • Lazily filtering out non-Cats

    Lazily filtering out non-Cats

    In a previous article, I discussed how to safely generate a list of subtypes from an original list defined on a…

  • Cats are not Dogs

    Cats are not Dogs

    Who in life has not tried to do this? Trying to treat a list of Animal as a list of a subtype. This does not compile in…

  • Teaching Agile gives student a fish

    Teaching Agile gives student a fish

    Teaching a student or colleague agile software development or more generally agile project management gives them a…

  • Agile Manifesto #5

    Agile Manifesto #5

    The Agile Manifesto [1] states that the left-hand side of the following are preferred over the right-hand side:…

社区洞察

其他会员也浏览了