The Darien Project
The Darien Project for Java code makes handling Java code failure easy. It’s well-known that error-handling code is buggy [1]. Using the library allows you to easily focus on the failure path to build better, working code more quickly.
You can ask questions at info@darien-project.org.
Quick Start
The call to m.getPage
below may fail in two ways: its internal HTTP GET might return a status value outside the 200 to 299 success range, or getPage
might have encountered an exception. Either
way, the failure path will be executed.
1public static void main(String[] args) {
2 Main m = new Main();
3
4 // Gets a webpage as a String and returns it wrapped inside an S type
5 S obj = m.getPage("https://www.example.com");
6
7 if(obj.eval()) {
8 String page = (String) obj.unwrap();
9
10 System.out.println("The success path");
11 } else {
12 System.out.println("The failure path");
13 }
14}
Darien Library tool support will write the code invocation for you, the if
, else
, and switch
, you see below so that you can focus on what you need to.
We handle the two failure cases like this (the implementation of getPage
is defined in getPage):
1public static void main(String[] argv) {
2 Main m = new Main();
3
4 // Returns a FailureValue, wrapping 404
5 S obj = m.getPage("https://www.example.com/nosuchpage");
6
7 if(obj.eval()) {
8 System.out.println("Success");
9 } else {
10 switch (obj) {
11 case FailureValue fv -> System.out.println(fv.getValue());
12 case FailureException fe -> System.out.println(fe.getException());
13 default -> System.out.println("As currently written, not possible.");
14 }
15 }
16}
The switch
on page
above is an example of pattern matching, released in Java SE 17 (https://openjdk.org/jeps/406) [2].
Running the above, attempting to retrieve https://www.example.com/nosuchpage
, results in a 404 being returned, wrapped in a FailureValue
.
As below, when getPage
is passed https://www.cannotfindthisdomain.com
, an instance of FailureException
is returned.
1 public static void main(String[] argv) {
2 Main m = new Main();
3
4 // Returns an instance of FailureException
5 S obj = m.getPage("https://www.cannotfindthisdomain.com");
6
7 if(obj.eval()) {
8 String page = (String) obj.unwrap();
9 System.out.println("Success");
10 } else {
11 switch (obj) {
12 case FailureValue fv -> System.out.println(fv.getValue());
13 case FailureException fe -> System.out.println(fe.getException());
14 default -> System.out.println("As currently written, not possible.");
15 }
16 }
17 }
All failure-describing types (FailureValue
, FailureException
and others) are subtypes of F
(see The Detail below) which in turn is a subtype of S
. S
’s eval
returns true
, whereas
eval
on F
and its subtypes returns false
. Within the failure path (the else), the appropriate failure instance (fv
or fe
) is created via the type switch.
That is it. You are done.
This approach focuses on the different kinds of failure, cleanly separating all cases, and tool supports write the handling code.
The Detail
S
is a type that wraps an instance and defines two methods. unwrap
returns the instance and eval
returns true
. Generics are not used. This is explained in Generics.
1public interface S {
2 public boolean eval();
3 public Object unwrap();
4}
F
is the root of all failure-describing types:
1public interface F extends S {
2}
All subtypes of F
override eval
to return false
.
The failure-describing types below (such as FailureValue
) are wrappers around an instance associated with the failure, such as a value or exception.
FailureValue
is defined as:
1 public interface FailureValue extends F {
2 public Number getValue();
3 }
FailureValue
wraps a Number
. This type is useful when an operation has failed and a code value associated with the failure is to be returned, as in the HTTP GET 404 above.
FailureException
wraps an exception in the same way:
1 public interface FailureException extends F {
2 public Exception getException();
3 }
getPage
When url
is https://www.cannotfindthisdomain.com
, getPage
returns a FailureException
that wraps the thrown java.net.UnknownHostException
.
If url
is https://www.example.com/nosuchpage
, getPage
will return a FailureValue
that wraps the number 404.
1 public S getPage(String url) {
2 try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
3 final HttpGet httpget = new HttpGet(url);
4
5 Result result = httpclient.execute(httpget, response -> {
6 return new Result(response.getCode(), EntityUtils.toString(response.getEntity()));
7 });
8
9 if(result.status_code >= 200 && result.status_code <= 299) {
10 return new Success(result.page);
11 } else {
12 return new FV(result.status_code);
13 }
14 } catch(java.io.IOException ioe) {
15 return new FExp(ioe);
16 } catch(Exception e) {
17 return new FExp(e);
18 }
19 }
Although getPage
looks perfectly reasonable, url
may be null. Or url
should be rejected if it does not use SSL (https), as implemented below.
1 public S getPage(String url) {
2 if(FailureUtils.oneIsNull(url)) {
3 return FailureUtils.theNull(url);
4 }
5
6 if(new URL(url).getProtocol().equals("http")) {
7 return new FAFC(url));
8 }
9
10 try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
11 final HttpGet httpget = new HttpGet(url);
12
13 Result result = httpclient.execute(httpget, response -> {
14 return new Result(response.getCode(), EntityUtils.toString(response.getEntity()));
15 });
16
17 if(result.status_code >= 200 && result.status_code <= 299) {
18 return new Success(result.page);
19 } else {
20 return new FV(result.status_code);
21 }
22 } catch(java.io.IOException ioe) {
23 return new FExp(ioe);
24 } catch(Exception e) {
25 return new FExp(e);
26 }
27 }
Note: Result
is a static class
defined in the same class as getPaage
that passes the response code and the retrieved webpage from execute
so it can be assigned to result
.
1 private static class Result {
2 public final int status_code;
3 public final String page;
4
5 public Result(int i, String str) {
6 this.status_code = i;
7 this.page = str;
8 }
9 }
Generics
Types S
and F
do not use generics. This means that you must explicitly cast the result of unwrap
. A generic S<T>
would remove the need for the cast. However, in the failure case
(which has more types), you would be required to enter the generic type for the success case, e.g., FailureValue<String>
, which is redundant as failure types are containers for failure objects.
Primarily, generics have not been used to ensure code brevity.
Using Interfaces
You will note that S
, F
, and all the failure-describing types, are Java interfaces. You use these types when using the library, as a consumer, as in the main
methods
in Quick Start above.
When you produce success and failure cases, you use an implementation class of these types, as in getPage (such as the class Success
).
As an engineer, you reason about success and failure and how to handle these cases using the types. You give these types concrete meaning at run-time by using the classes in org.darien.types.impl
. In
this code design, classes are purely a mechanism for expressing code and its reuse.
Focusing on Failure Leads to More Robust Code
By focusing on failure with the above approach, we see that:
Any method parameter can cause your code to fail
All code paths are terminated at a
return
Any search code can fail
The Darien approach is to check parameter values for null
, returning an appropriate failure instance. The library supports you here with its calls to FailureUtils.oneIsNull
and FailureUtils.theNull
.
For point 2., the Darien approach is to return exceptions wrapped in a FailureException
. This style is preferred over throwing an exception because where an exception is caught might be a long way
from where it is generated, reducing options for addressing the issue. However, adopting this style is a matter of preference.
All code that searches for something (or that looks something up or relies on something being present) can fail when the item assumed to be there is absent. To highlight this, the following extracts the right-hand side of a string containing a hyphen of the form “lhs-rhs”.
1 private String rhs(String input) {
2 return input.split("-")[1];
3 }
If input
is hyphen-ated
, rhs
will return ated
. But if input
is hyphenated
, an ArrayIndexOutOfBoundsException
will be raised. This code handles that failure case:
1 private S rhs(String input) {
2 if(FailureUtils.oneIsNull(input)) {
3 return FailureUtils.theNull(input);
4 }
5
6 if(input.indexOf("-") == -1) {
7 return new FailureValue(-1);
8 } else {
9 return new Success(input.split("-")[1]);
10 }
11 }