Using Throwable for Things Other than Exceptions
Asked Answered
A

6

6

I have always seen Throwable/Exception in the context of errors. But I can think of situations where it would be really nice to extend a Throwable just to break out of a stack of recursive method calls. Say, for example, you were trying to find and return some object in a tree by the way of a recursive search. Once you find it stick it in some Carrier extends Throwable and throw it, and catch it in the method that calls the recursive method.

Positive: You don't have to worry about the return logic of the recursive calls; since you found what you needed, why worry how you would carry that reference back up the method stack.

Negative: You have a stack trace that you don't need. Also the try/catch block becomes counter-intuitive.

Here is an idiotically simple usage:

public class ThrowableDriver {
    public static void main(String[] args) {
        ThrowableTester tt = new ThrowableTester();
        try {
            tt.rec();
        } catch (TestThrowable e) {
            System.out.print("All good\n");
        }
    }
}

public class TestThrowable extends Throwable {

}

public class ThrowableTester {
    int i=0;

    void rec() throws TestThrowable {
        if(i == 10) throw new TestThrowable();
        i++;
        rec();
    }
}

The question is, is there a better way to attain the same thing? Also, is there something inherently bad about doing things this way?

Albers answered 1/8, 2011 at 3:22 Comment(4)
possible duplicate of Why not use exceptions as regular flow of control?Alaric
Might be a duplicate but a good question which I would not have come across if you had not asked +1Accelerando
@Accelerando - It's a horrible. People have used exceptions in lieu of control structures with horrible results. And yet, people keep asking this (oblivious from real-world lessons against it.)Assignor
stack traces are not necessarily generated unless used. There is also a hard way to disable it... guess? Either create a stock exception and throw it, or create a new (Runtime)Exception class that overrides fillInStackTrace()Katushka
B
8

Actually, it's an excellent idea to use exceptions in some cases where "normal" programmers wouldn't think of using them. For instance, in a parser that starts down a "rule" and discovers that it doesn't work, an exception is a pretty good way to blow back to the correct recovery point. (This is similar to a degree to your suggestion of breaking out of recursion.)

There is the classical objection that "exceptions are no better than a goto", which is patently false. In Java and most other reasonably modern languages you can have nested exception handlers and finally handlers, and so when control is transferred via an exception a well-designed program can perform cleanup, etc. In fact, in this way exceptions are in several ways preferable to return codes, since with a return code you must add logic at EVERY return point to test the return code and find and execute the correct finally logic (perhaps several nested pieces) before exiting the routine. With exception handlers this is reasonably automatic, via nested exception handlers.

Exceptions do come with some "baggage" -- the stack trace in Java, eg. But Java exceptions are actually quite efficient (at least compared to implementations in some other languages), so performance shouldn't be a big issue if you're not using exceptions too heavily.

[I'll add that I have 40 years of programming experience, and I've been using exceptions since the late 70s. Independently "invented" try/catch/finally (called it BEGIN/ABEXIT/EXIT) ca 1980.]

An "illegal" digression:

I think the thing that is often missed in these discussions is that the #1 problem in computing is not cost or complexity or standards or performance, but control.

By "control" I don't mean "control flow" or "control language" or "operator control" or any of the other contexts where the term "control" is frequently used. I do sort of mean "control of complexity", but it's more than that -- it's "conceptual control".

We've all done it (at least those of us that have been programming for longer than about 6 weeks) -- started out writing a "simple little program" with no real structure or standards (other than those we might habitually use), not worrying about its complexity, because it's "simple" and a "throwaway". But then, in maybe one case in 10 or one case in 100, depending on the context, the "simple little program" grows into a monstrosity.

We loose "conceptual control" over it. Fixing one bug introduces two more. The control and data flow of the program becomes opaque. It behaves in ways that we can't quite comprehend.

And yet, by most standards, this "simple little program" is not that complex. It's not really that many lines of code. Very likely (since we are skilled programmers) it's broken into an "appropriate" number of subroutines. Run it through a complexity measuring algorithm and likely (since it is still relatively small and "subroutine-ized") it will score as not particularly complex.

Ultimately, maintaining conceptual control is the driving force behind virtually all software tools and languages. Yes, things like assemblers and compilers make us more productive, and productivity is the claimed driving force, but much of that productivity improvement is because we don't have to busy ourselves with "irrelevant" details and can focus instead on the concepts we want to implement.

Major advancements in conceptual control occurred early in computing history as "external subroutines" came into existence and became more and more independent of their environments, allowing a "separation of concerns" where a subroutine developer did not need to know much about the subroutine's environment, and the user of the subroutine did not need to know much about the subroutine internals.

The simple development of BEGIN/END and "{...}" produced similar advancements, as even "inline" code could benefit from some isolation between "out there" and "in here".

Many of the tools and language features that we take for granted exist and are useful because they help maintain intellectual control over ever more complex software structures. And one can pretty accurately gauge the utility of a new tool or feature by how it aids in this intellectual control.

One if the biggest remaining areas of difficulty is resource management. By "resource" here, I mean any entity -- object, open file, allocated heap, etc -- that might be "created" or "allocated" in the course of program execution and subsequently need some form of deallocation. The invention of the "automatic stack" was a first step here -- variables could be allocated "on the stack" and then automatically deleted when the subroutine that "allocated" them exited. (This was a very controversial concept at one time, and many "authorities" advised against using the feature because it impacted performance.)

But in most (all?) languages this problem still exists in one form or another. Languages that use an explicit heap have the need to "delete" whatever you "new", eg. Opened files must be closed somehow. Locks must be released. Some of these problems can be finessed (using a GC heap, eg) or papered over (reference counts or "parenting"), but there's no way to eliminate or hide all of them. And, while managing this problem in the simple case is fairly straight-forward (eg, new an object, call the subroutine that uses it, then delete it), real life is rarely that simple. It's not uncommon to have a method that makes a dozen or so different calls, somewhat randomly allocating resources between the calls, with different "lifetimes" for those resources. And some of the calls may return results that change the control flow, in some cases causing the subroutine to exit, or they may cause a loop around some subset of the subroutine body. Knowing how to release resources in such a scenario (releasing all the right ones and none of the wrong ones) is a challenge, and it gets even more complex as the subroutine is modified over time (as all code of any complexity is).

The basic concept of a try/finally mechanism (ignoring for a moment the catch aspect) addresses the above problem fairly well (though far from perfectly, I'll admit). With each new resource or group of resources that needs to be managed the programmer introduces a try/finally block, placing the deallocation logic in the finally clause. In addition to the practical aspect of assuring that the resources will be released, this approach has the advantage of clearly delineating the "scope" of the resources involved, providing a sort of documentation that is "forcefully maintained".

The fact that this mechanism is coupled with the catch mechanism is a bit of serendipity, as the same mechanism that is used to manage resources in the normal case is used to manage them in the "exception" case. Since "exceptions" are (ostensibly) rare, it is always wise to minimize the amount of logic in that rare path, since it will never be as well tested as the mainline, and since "conceptualizing" error cases is particularly difficult for the average programmer.

Granted, try/finally has some problems. One of the first among them is that the blocks can become nested so deeply that the program structure becomes obscured rather than clarified. But this is a problem in common with do loops and if statements, and it awaits some inspired insight from a language designer. The bigger problem is that try/finally has the catch (and even worse, exception) baggage, meaning that it is inevitably relegated to be a second-class citizen. (Eg, finally doesn't even exist as a concept in Java bytecodes, beyond the now-deprecated JSB/RET mechanism.)

There are other approaches. IBM iSeries (or "System i" or "IBM i" or whatever they call it now) has the concept of attaching a cleanup handler to a given invocation level in the call stack, to be executed when the associated program returns (or exits abnormally). While this, in its current form, is clumsy and not really suited to the fine level of control needed in a Java program, eg, it does point at a potential direction.

And, of course, in the C++ language family (but not Java) there is the ability to instantiate a class representative of the resource as an automatic variable and have the object destructor provide "cleanup" on exit from the variable's scope. (Note that this scheme, under the covers, is essentially using try/finally.) This is an excellent approach in many ways, but it requires either a suite of generic "cleanup" classes or the definition of a new class for each different type of resource, creating a potential "cloud" of textually bulky but relatively meaningless class definitions. (And, as I said, it's not an option for Java in its present form.)

But I digress.

Bumble answered 1/8, 2011 at 3:51 Comment(3)
I agree the comparison to goto's is nonsensical. Clearly goto's can be horrific, nonlinear, and all over the place. Throws are linear on the stack. If you throw, someone has to catch it on the stack somewhere. I have no idea why people stick to this comparison.Albers
@Daniel - there are always edge cases where an exception in lieu of a typical control structure makes sense (as in your 'parser' example). But in the general case, I'm sure you know this is not the case. I mean, seriously, I fail to see such a thing would be beneficial for the general case (which the original post/question illustrated with its example.)Assignor
No, the original poster said "I can think of situations" where it seemed to him that exceptions are a wise choice. He is correct. Nothing should be universally used "for the general case" -- if it were possible there wouldn't be a need for programmers.Bumble
D
4

Using exceptions for program control flow is not a good idea.

Reserve exceptions for exactly that, for circumstances that are outside of the normal operating criteria.

There are quite a few related questions on SO:

Dellinger answered 1/8, 2011 at 3:26 Comment(1)
Thanks for the links, I couldn't find a relevant link for the life of me, probably because I didn't use "control flow".Albers
A
2

The syntax becomes wonky because they're not designed for general control flow. Standard practice in recursive function design is to return either a sentinel value or the found value (or nothing, which would work in your example) all the way back up.

Conventional wisdom: "Exceptions are for exceptional circumstances." As you note, Throwable sounds in theory more generalized, but except for Exceptions and Errors, it doesn't seem designed for broader use. From the docs:

The Throwable class is the superclass of all errors and exceptions in the Java language.

Many runtimes (VMs) are designed not to optimize around throwing exceptions, meaning they can be "expensive". That doesn't mean you couldn't do this, of course, and "expensive" is subjective, but generally this isn't done, and others would be surprised to find it in your code.

Alaric answered 1/8, 2011 at 3:27 Comment(5)
"Many runtimes (VMs) are designed not to optimize around throwing exceptions" because "Exceptions are for exceptional circumstances." Kinda circular reasoning, what?Bumble
I agree that exceptions are for exceptional circumstances, and that is why I said Throwable. One would have hoped that Throwable would have other uses than inevitably becoming an Exception. Unfortunately, Java's try/catch/finally screams exception. I tried, failed, caught, cleaned up. Too bad really. It didn't have to be that way.Albers
@delmet: Understood. It seems that Throwable is just a base class intended to play host to exceptions and also Errors. From the Throwable docs: "The Throwable class is the superclass of all errors and exceptions in the Java language." Despite the pleasant name, I don't think the language designers intended it to be much more broadly useful than that. :)Alaric
@Albers -- Java's exception implementation is considerably better than most. Progress in language design proceeds at glacial speed.Bumble
The name "Throwable" is a tell-all of its original function. To be thrown (or raised), by a throw (or raise in Ada parlance). Throw (and raise) has been in PL parlance for a long time indicating a very specific error-related nature.Assignor
A
1

The question is, is there a better way to attain the same thing? Also, is there something inherently bad about doing things this way?

Regarding your second question, exceptions carry a significant run-time burden, regardless of how efficient the compiler can be. That alone should speak against using them as control structures in the general case.

Furthermore, exceptions amount to controlled gotos, almost equivalent to long jumps. Yes, yes, they can be nested, and in languages like Java, you can have your nice 'finally' blocks and all. Still, that's all they are, and as such, they are not meant to be general-case replacements for your typical control structures. More than four decades of collective, industrial knowledge tells us than, in general, we should avoid such things UNLESS you have a very valid reason to do so.

And that goes to the hearth of your first question. Yes, there is a better way (taking your code as example)... simply use your typical control structures:

// class and method names remain the same, though using 
// your typical logical control structures

public class ThrowableDriver {
    public static void main(String[] args) {
        ThrowableTester tt = new ThrowableTester();
        tt.rec();
        System.out.print("All good\n");
        }
    }
}

public class ThrowableTester {
    int i=0;

    void rec() {
        if(i == 10) return;
        i++;
        rec();
    }
}

See? Simpler. Less lines of code. No redundant try/catch or unnecessary exception throwing. You achieve the same.

In the end, our job is not to play with language constructs, but to create programs that are sensible, sufficiently simple for a maintainability point of view, with just enough statements to get the job done and with nothing else.

So, when it comes to the example code that you provided, you have to ask yourself: what did I get with that approach that I cannot get when using typical control structures?

You don't have to worry about the return logic of the recursive calls;

If you don't worry about the return logic, then simply ignore the return or define your method to be of type void. Wrapping it in a try/catch simply makes the code more complex than necessary. If you don't care about the return, I'm sure you care about the method to complete. So all you need is to simply call it (as in the code sample I provided with this post).

since you found what you needed, why worry how you would carry that reference back up the method stack.

It is cheaper to get push the return (pretty much an object reference in the JVM) to the stack before completion of the method than to do all the book keeping involved with throwing an exception (running epilogs and filling up a potentially big stack trace) and catching it (traversing the stack trace.) JVM or not, this is basic CS 101 stuff.

So, not only it is more expensive, you still have to type more characters to code the same thing.

There is virtually no recursive method that you can exit via a Throwable that you cannot re-write using your typical control structures. You need to have a very, very, but very good reason to use an exception in lieu of control structures.

Assignor answered 1/8, 2011 at 16:57 Comment(16)
My example was to illustrate how throwable can be used in a "positive" manner, as opposed to as an exception. I agree is an extremely poor usage for that problem (actually the whole code is illustrative, and poor in general). But take the case where you need to find something recursively. Now you need to carry back a sentinel that marks that the search is over, and the found value. In that situation, a carrier throwable makes things more intuitive. Load up the result, and throw it to the caller. In general, I agree that Java's syntax does not do a good job supporting throwables in the positiveAlbers
Now take your simple example and add a return code that needs to be tested after each call, and, based on that return code you either return or continue with another call in sequence. String together a half-dozen calls back-to-back like this, only interleave between them objects that you allocate and need to be deallocated before you return. Try to manage that with if/then/else and you have a nightmare. Do it with exception handlers and it's a bit ugly but easily manageable.Bumble
@Albers - the use of a throwable to indicate an exception is the "positive" manner. That's the intention of this tool. I can use a hammer like a fork, but possibility does not imply positivity.Assignor
@Daniel - In a case like this, and for very specific cases, then yes, an exception or a long jump will do the trick better (I've done that in several occasions to good effect in code simplification.) But those are edge cases, not general cases. For general cases, it is an unjustifiable monstrosity. Bring a specific, concrete case that justifies this, and then we have an argument. So far, none has been presented, and to make this argument, we need more than just a simple case (or even a complex, but general case.)Assignor
@Daniel - I'll add stuff to the bottom of my answer at a later time to elucidate my POV better.Assignor
@Albers - "Now you need to carry back a sentinel that marks that the search is over, and the found value. In that situation, a carrier throwable makes things more intuitive." - More intuitive is a subjective measure. The overwhelming majority of programming practice does not do this, ergo, it is not intuitive for whoever ends up maintaining your code. As for the sentinel, as I said in my answer, it is more effective in terms of speed and size than a throwable. Simply compile code for both options and inspect the bytecode and see for yourself ;)Assignor
@luis -- part of the problem is that exceptions have been relegated to "exceptional situations", sort of a self-fulfilling prophecy, so that they never quite get generalized to the point where they don't seem foreign in "normal" code.Bumble
@Albers - con't - "But take the case where you need to find something recursively" - if you have a recursive function that do requires a throwable for simplicity, it is time to revisit that function. Honestly, I've only encountered, maybe 3 times in my life where such an option was absolutely needed. In all other such implementations, they were just bad implementations. Seriously, I need to see an actual example of a recursive function that legitimately needs such a thing, for I cannot think of one. Good recursive functions are typically simple ones. Anything else is a red flag.Assignor
@Daniel - I see your point (from a PL theory/design POV). I would posit the counter-argument that for "normal" cases, we should adhere to the typical control structures delineated by the Bohm-Jacopini theorem, even if it means carrying additional book keeping (in the form of sentinels and control variables). In fact, I'm actually in favor of getting rid of exceptions (which is nothing but a glorified long jump) altogether in favor of the defer/panic mechanism proposed by Google Go's.Assignor
The view that exceptions are a glorified long jump is part of the problem, not the solution. Though I know nothing of defer/panic (not that I like the sound of it) -- do you have a reference?Bumble
But it is a glorified long jump. Upon a throw, control goes irrevocably to the closest catch block (an effective label). The only difference (actually one of the many) from a long jump is that the label to jump to is not arbitrarily fix and that it provides a structure one can reason about. And I don't see that as a problem (nor I see an advantage to use such a construct - in the general cases - in lieu of structured control structures except in extreme edge cases.)Assignor
con't - They are not without fault, though, which is why I'd prefer to see something more "high-level" with more automated (and robust) book keeping with less of a programmer's intervention and less boiler-plate. Enter "defer/panic/recover" : blog.golang.org/2010/08/defer-panic-and-recover.htmlAssignor
@luis -- think of this scenario: you have a function stack, and each function does something like fi.apply(fi+1.apply(somestring)). fi's might each be some string to string transformer. The example is not a recursive one. Say that it is possible that any one of them might return null. Well, then, we have a problem. Either the beautiful code is going to go down the toilet by null checking logic, or we are going to jump up to the mother function and handle it there. Which is more preferable. This is my current situation actually, And the application is in Hadoop where CPU efficiency is the...Albers
...least of my concern. Good code with linear jump on the stack, or crappy code with status quo?Albers
@Albers You are playing with the semantics of "positive". - meaning?Assignor
@delmet- as for your current situation with Hadoop, now we are talking about a specific situation where indeed what you are proposing makes sense. That is completely valid. But you can also recognize that this is an edge case. Your alternative solution is not "positive" usage in the general case. I myself have relied on longjmp and gotos for very specific cases in which the result is far more efficient (and elegant). But those were edge cases as well, and I would never propose to do the same for the general case (because they would not constitute a "positive" usage.)Assignor
S
0

Just. Don't.
See: Effective Java by Joshua Bloch, p. 243

Supernational answered 1/8, 2011 at 17:15 Comment(1)
+1 for you (for quoting Bloch and to undo the neg rep). I'll never understand why somebody would neg rep you for quoting what is pretty much the bible on how to write good Java code without even leaving a comment or reason for it (a most juvenile thing to do.)Assignor
D
-1

I do not know if it was a good idea or not, but while designing a CLI (without using prepared libraries) it occurred to me that a natural way to handle going back from a position in the application without messing up the system stack is to use a Throwable (if you just call the method from which you came to this one you will get STACK OVER FLOW if someone say goes forward and backward about 255 times in application menues). Since going back using a Throwable is independent from where you are in the application it gave me the power to make the methods abstract (in the literal sense) , i.e., all of the menus consisting of some entries of class X were handled with one method.

Daw answered 4/4, 2021 at 21:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.