1

I asked this questions on StackOverflow but it's definitely a bit too broad. Even for this website, although the question is about software design, it might not be enough "focused".

I am working on a family of java 8 applications where a bit of refactoring is definitely overdue. Right now I'm focusing on the error handling, but the problem can be stated more generally: how would you represent the concept of "alternative result values"?

To give a more concrete explanation, let's say I have a source I can query in order to retrieve some data:

T value = source.get(x)

For a variety of reason outside my control this method might not be able to produce the value of type T I requested. In this case I would like, as the caller, to know what went wrong. Be it some DB connection problem or just the fact that what I was searching for does not exist.

The prolem gets even harder when a single procedure needs to gather data from different sources each possibly requiring the values retrieved earlier from the other sources:

T procedure(p) {
    A a = sourceA.get(p);
    B b = sourceB.get(p,a);
    C c = sourceC.get(p,a,b)
    return new T(p,a,b,c);
}

Each of these sources would have its own set of "response codes" which are returned when something prevents them from producing the expected result.

As of now in my applications this issues are handled in a variety of questionable ways: sometimes a null is returned, sometimes (for the applications that are webapps) HTTP codes are used deep inside the "logic" layer of the application and sometimes an empty/uninitialized version of the expected object is returned just for the sake of not returning null.

I would like to find a general-enough way of handling this issue so that, by adopting it in all the applications, no one needs to invent a solution on the spot (further increasing the entropy) when a problem like this arises.

To choose the "strategy" I am focusing on three main (not necessarily indipendent) points:

  1. how the error is returned to the caller
  2. how the error is handled (here the ease of use is the main concern)
  3. how much freedom I have in the representation of the error (sometimes a code might not be enough and more information needs to be stored in the error)

Here are the solutions I thought about divided into two categories depending on how the first point (error returning) is addressed.

Result object. This is essentially a wrapper object (Result<E,T> or Result<T>) which contains a "response code" and, optionally, a value of type T if everything worked fine.

This solution allows for the error to be returned in a "procedural" fashion

Result<T> procedure(p) {
    Result<A> resA = sourceA.get(p);
    if (!resA.isOK())
        return Result.of(resA.getCode())
    A a = resA.getValue();
    Result<B> resB = sourceB.get(p,a);
    if (!resB.isOK())
        return Result.of(resB.getCode())
    B b = resB.getValue();
    Result<C> resC = sourceC.get(p,a,b)
    if (!resC.isOK())
        return Result.of(resC.getCode())
    C c = resC.getValue();
    return Result.of(new T(p,a,b,c));
}

or in a "functional" one (it is essentially an Either<L,R>).

Result<T> procedure(p) {
    return sourceA.get(p)
        .flatMap(a -> sourceB.get(p,a)
            .flatMap(b -> sourceC.get(p,a,b)
                .flatMap(c -> Result.of(new T(p,a,b,c))));
}

The functional solution is "conceptually" cleaner but a lot harder to read and i think it looks a bit out of place in java. The procedural one looks fine but it's a lot more prone to programming error considering that no one forces you to check the result with isOK before calling getValue/getCode.

Here the handling depends directly on how I choose to represent the error/status. The simplest solution would be to use an enum. In that case the caller would handle everything with a switch-case:

Result<T> procedureResult = precedure(p);
if (procedureResult.isOK()){ ...do stuff... }
else {
    switch(procedureResult.getCode()) {
        case CODE1 : ...
        case CODE2 : ...
        ...
        default : ...
    }
}

This is a bit limiting because enums are not very customizable.

To be more flexible I might use a different class for each error. This gives me complete freedom but adds quite a bit of complexity in the "handling phase". In fact, the only way (I can think of) to handle all the different objects in a general way is to use a visitor pattern. This is the exact opposite of "ease of use" mostly because if different errors need to be handled differently depending on the context I might end up defining a Visitor implementation for each possible combination. Furthermore I am not the only one working on this code and introducing the visitor pattern would require convincing everybody that the benefits justify the counter-intuitive setup they will be forced to work with.

As a note I'll point out that a variation on the Result object might be some sort of C-like status:

Status procedure(p, resultContainer)

Or an "inverted" version of the same idea:

Optional<T> procedure(p, statusContainer)

But they present the same issues as Result and more.

Checked Exceptions. Here the errors can have whatever shape they want and are easily returned and handled with the famous goto shenanigans.

T procedure(p) throws SourceAException1,
                      SourceAException2,
                      SourceBException,
                      SourceCException {
    A a = sourceA.get(p);
    B b = sourceB.get(p,a);
    C c = sourceC.get(p,a,b)
    return new T(p,a,b,c);
}
try {
    T = procedure(p);
}
catch(SourceAException1 sae1){ ... }
catch(SourceAException2 sae2){ ... }
catch(SourceBException sbe){ ... }
catch(SourceBException sbc){ ... }

On paper this look like the better solution (note that I like the "errors annotations" in the method signature even if it's a bit verbose) but there are 2 problems:

  1. in java using exception as an "alternative return" is considered bad practice (by some an anti-pattern even) because an exception signals an exceptional occurrence and should not be used for "expected behaviour". Moreover, by generalizing the problem to "status codes" and not necessarily errors, I've thrown myself quite a bit outside of the exceptions' scope and intended use.

Even assuming that this might actually be just a matter of preference, what worries me is:

  1. the exceptions are slow. From what I found it seems this is caused by the construction of the stack trace. In order to alleviate this problem I found out that you can make the stacktrace non-writable but this looks very non-standard and I am not even sure it would make that much of a difference.

At this point it's obvious I need to find a compromise. My most recent idea is to give up a bit of "representation freedom" and (reluctantly) some type safety and use an enum where each value of the enum has a Map<String,Object> (maybe even Map<String,String>). The key-value pairs are general enough to let me represent almost anything I can think of. With a builder method (here called "with") that adds the pair to the enum value, the creation and returning of the code would look like this:

class Source {
    Result<T> get(x) {
        ...
        if (everything-is-ok) return Result.of( new T(...) )
        else return Result.of(CODE1.with("key1",val1).with("key2",val2)...)
    }
}

and the caller could handle everything with a switch-case (and probably some terrible type cast).

Of all the solutions I can think of, despite what most people say, I find the checked exceptions to be the cleanest. Nevertheless (even if I expect no state-altering logic to be executed in the catch clause) it would definitly be an abuse, even in their "lightweight" version without stacktrace.

So, at the end of it all, I will ask you guys:

  1. should I forget about using exceptions this way?
  2. do you know some other solution that better addresses some of the concerns I listed here?

Thank you

6
  • I'm somewhat sure, the first snippet can be easily moved to Chain of responsibility pattern. The !isOk then return code is the breaking condition to halt the chain. You only have to get rid of this Result.of(result.getCode()) I'm sure result is very capable of solving this internally.
    – Laiv
    Commented Jun 3, 2023 at 8:53
  • Another pattern that is worth testing is the pipeline pattern. I always struggle to see/understand the differences between these two patterns. The chain has certain advantages I have tested myself, for example, you can fork/join the chain with conditional / merge links.
    – Laiv
    Commented Jun 3, 2023 at 8:57
  • It looks like the chain of commands can be used when a request is dispached and you need to find who, in some sort of hierarchy, can handle it. The pipeline is closer to what I need since it looks like function composition (am I intepreting them right?). The problem is that, since all the "pipes" take exactly one argument, for each step I need to define a new "container" object which gathers all the results from the previous calls. Anyway, I was focused more on the "failure" side of the computation: how do I communicate to the caller that something failed and WHY it failed Commented Jun 5, 2023 at 19:30
  • Yes, you can implement both has a composition of functions. As for the pipeline's parameter, you can pass as many arguments as you need. I think (p, result) would be ok. p is a common denominator among all the functions and result can be an aggregate, or can be recalculated in each "step function". Given the unknown number of "T" types and "sources", I would not use generics.
    – Laiv
    Commented Jun 6, 2023 at 6:18
  • 1
    "exceptions are slow" - are you talking about the performance cost of having exceptions or throwing exceptions? Have you observed this to be a real problem?
    – JacquesB
    Commented Jul 5, 2023 at 6:32

2 Answers 2

1

how would you represent the concept of "alternative result values"?

It's not clear that's the right question to ask.

C, go, lua, rust, and others return a (result, is_valid) tuple, with the expectation that caller only uses result after verifying is_valid. The literature reveals a long history of application programmers failing to correctly use such an API 100% of the time, though this certainly improves when the compiler offers some help.

If you want to augment a return type of T with httpStatus or elapsedTime, then embellishing the legacy code with a façade layer that's aware of the additional fields is straightforward. But it sounds like the pressing problem with the legacy code is inconsistent use of null or empty container to denote the concept of not is_valid.

In the code I write, usually a better question to ask is, "How can I ensure my code only returns valid results?" I find that DbC offers a very workable response to that. In Common Lisp, scheme, python, C++, and many other languages we can specify that a function returns valid result of type T or it blows up with an exception, it doesn't return a value at all. I recommend you embrace java exceptions.

If your legacy code "frequently" does exceptional things like failing to produce a valid type T result, then you may want to write adaptor code which reports the failure in a uniform way. Sometimes null is the best answer. But often it's better to return an empty container or an empty object. Suppose T is Employee, and a DB query found zero matching rows. Rather than return null you might return an EmptyEmployee. This is still typesafe, without threat of NPE. It boils down to evaluating whether you're happy with the design of a legacy Public API, versus whether you feel it should be changed or perhaps papered over with another layer.


The "checked" aspect of java exceptions has caused some grief.

Consider omitting exceptions from your method signatures, preferring to wrap checked exceptions:

        throw new RuntimeException("you lose", e);

You should definitely create a tree of one or more app-specific exceptions that inherit from RuntimeException, so callers can conveniently catch just what they need to.

3
  • Swift has enums where you can’t access a result without checking that it is valid, so that problem goes away.
    – gnasher729
    Commented Jun 2, 2023 at 19:21
  • In this context I am iterpreting exceptions too as an "alternative result value". Not because that is what they necessarily are, but because that is how I would end up using them as (again, in this context). What I am looking for is, as you said, a mechanism which "reports failure in a uniform way". (result, is_valid) almost does it but I wold like to know why my result is not valid. I could define a set of status codes for each possible "fallible" call but, with the intent of finding a "general" solution, I ended up writing my, maybe unclear, question. Commented Jun 5, 2023 at 19:45
  • Sounds like an enum. Consider the C solution, where errno == 0 is success, and anything else is an informative error code. Your returned tuple could offer something similar, with a bool convenience function to abstract away the details. Alternatively, perhaps you feel a human-readable diagnostic string is needed, plus a structured error code. // The compilers for some languages offer each caller assistance in correctly consuming Public API results. In a java context, you might wish to rely on exceptions, since throwing one means the mainline calling code never obtains an invalid result.
    – J_H
    Commented Jun 5, 2023 at 22:09
1

Exceptions vs Result

Exceptions are the norm in Java . I suspect you've slightly mistaken the intent in various pieces of advice around misuse of exceptions, and in your head written them off.

The Result type (an Optional with an extra type parameter for failure information) is also a valid approach. It has the same downside as checked exceptions, which is that every caller needs to be specific about how it handles the possibility of failure. And although I just said exceptions are the norm, I'm not against using Result. Since Java 8 there tends to be a shift in the balance between functional and imperative styles, away from mostly imperative and towards a mix of the two . However, as with all functional style language features, I usually stop to consider the other collaborators' level of comfort. For instance, in my current workplace I'll use a forEach or a collect with List objects, but wouldn't chance something requiring a flatMap.

You could view exceptions as essentially the same mechanism as Result, but with the wrapping and unwrapping built into the language. For instance, you can think of every method that returns <T> as returning Result<T, Throwable> under the hood and every invocation being automatically wrapped in a handler that either successfully unwraps the successful result, or triggers its own early return with Result.ofError(...).

Rather than errors being "return codes" with a type like int, errors are preferably some type of object that can be interrogated to give us any additional information that might be relevant to that particular state, or even have their own behaviour. Since Result and Exception are similar ideas, error objects and Throwable objects are similar ideas. And Since they are objects, we can construct heirarchies that allow us to write handlers at different levels such as catch (FileNotFoundException e) vs catch (IOException e) vs catch (Throwable t). The language ensures completeness in the case of checked exceptions, and allows you to catch only as specifically as you like in the case of unchecked exceptions.

You compared exceptions to goto which is not a fair comparison. The problem with goto is the difficulty in tracking state by mutable variables when compared to using function calls and their return values. Whereas exception handling is primarily still based on the call stack and using return values.

HTTP status codes in Java

In Java, HTTP status codes are already mapped to exceptions. For examples there are the exceptions NotFoundException for 404 and ServerErrorException for 5xx. In core libraries, these tend to be checked exceptions. Critics talk about the profileration of try/catch or redeclaring throws that tends to come up with checked exceptions, while others like that the language ensures every possibility is accounted for.

In many frameworks, Spring for example, unchecked exceptions are used instead but for the same purpose. Note that in Java you can still use throws to declare that your method might throw an unchecked exception, and these frameworks often do. It provides useful information (in the IDE) about what can go wrong, without requiring programmers to necessarily write try/catch or redeclare throws. This is largely possible because these frameworks have decent enough error handlers that sit near the top of the call stack. For instance, web servers would serve an error page, rather than crashing the server program.

4
  • Maybe what I wrote gave the impression that i was "writing them off" but i wasn't! The "paragraph" about exceptions is short because they do exactly what I need and, on top of it, are the solution which feels closest to the language. What I said about the goto was the "obligatory joke" which was probably unnecessary. The philosophical question of "when is appropriate to use exceptions" as a design tool is subordinate (ad as I said, maybe a matter of preference) to what it might cost to use them. Commented Jun 5, 2023 at 20:09
  • ... What stops me is just what i said in the question: exceptions come with an added computational cost I have no control over. And since some of the applications I'm working on receive a lot of requests, I'm worried about the effect an extesive use of exceptions might have. Maybe some well focused load tests could lighten a bit my heavy heart. Commented Jun 5, 2023 at 20:09
  • ... In any case what you wrote in the second paragraph is exactly how I view checked exceptions (at least in this context). It's a shame it's not possible to respond inline. Commented Jun 5, 2023 at 20:12
  • Personally I'd advocate a hybrid approach. Something like Optional is great for the "data not found" case, which is something which you can be reasonably be expected to anticipate and handle in your code. Exceptions - preferably unchecked - are great for the "something went wrong, nothing I can do about it" cases. Commented Aug 22, 2023 at 21:14

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.