delmet
delmet

Reputation: 1013

Using Throwable for Things Other than Exceptions

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?

Upvotes: 6

Views: 1794

Answers (6)

Matin Yousefi
Matin Yousefi

Reputation: 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.

Upvotes: -1

Hot Licks
Hot Licks

Reputation: 47699

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.

Upvotes: 8

Ben Zotto
Ben Zotto

Reputation: 71008

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.

Upvotes: 2

Martin
Martin

Reputation: 425

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

Upvotes: 0

luis.espinal
luis.espinal

Reputation: 10519

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.

Upvotes: 1

Mitch Wheat
Mitch Wheat

Reputation: 300499

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:

Upvotes: 4

Related Questions