

Rethinking try/catch - Tangaroa

Consider these changes to the try/catch model of C++ and Java:<p>* Every block is a try{} block. The "try" keyword is dropped. All exceptions will filter upwards until they reach a block that catches the exception.<p>* The new "recover" keyword returns from a "catch" block to the next line of the context that threw the exception. The exception has a ".scope" object that allows access to the variables that were in scope at the time that the exception was thrown. Whenever an exception happens, programmers could twiddle a few variables and set the program back to where it was before.<p>Has any language already done this or something similar? How would this affect program design, code quality, and readability? What would language developers need to do to implement these features, and how would it impact performance?
======
beagle3
Common Lisp has a "condition system" that can do that (and with call/cc you
can build it yourself in e.g. scheme).

GWBASIC of old had an "on error goto / on error gosub" that could branch to
another part of the code on exception, and that part of code could always
"resume" -- although there weren't any user defined exceptions, so it's not
quite equivalent (and the scope of the "on error" handler was usually smaller
than global)

------
apenwarr
I once read some advice that totally changed the way I think about exceptions:
"The ratio of try/finally to try/catch in your programs should be about 10:1."
That is, if you're doing a lot of specialized try/catch, you're doing it
wrong. (And in C++ you generally use RAII instead of try/finally, but same
idea.)

When you do it this way, exceptions make your code _cleaner_ , not uglier, and
_more_ readable, not less.

To relate this to the OP, the proposal here implies a naive idea of what it
takes to "recover" from an exception. It's very rare to be able to recover at
the low level, and even if you can do it, it's hard to be 100% sure you got it
right. Low-level code includes _details_ that you're trying to hide from the
caller; if the caller has to be able to fix the details, you've ruined your
abstraction. If an inner library has a failure, it's kind of unlikely that the
caller knows how to recover from that specific detailed failure so you can
pick up where you left off. What you _actually_ want is to just let the caller
know there was a problem and then retry the whole big operation.

And hence the 10:1 ratio. Lots of try/finally blocks all the way into the deep
inner library, and _one_ try/catch block near the top: when your mainloop
catches a critical problem, see if it can recover. Mostly this will be by
fixing something (possibly asking the user to correct some input, or waiting a
bit in case the network server had a problem) and then retrying the whole
thing by just calling the function over again.

Someone in another comment suggested that an example of the "recover"
technique is when a kernel fixes up an exception (say a page fault) and then
resumes the user process. I disagree; in my opinion, you should think of
userspace calling the kernel, not the other way around. Even accessing memory
is a kernel call (albeit usually a highly optimized one). A page fault is not
really an "exception", it's just a slow-path memory access. You wouldn't
expect it to change your program flow, so it tries hard not to. It's perfectly
okay for an "inner" function (kernel) to recover itself so as not to disturb
the abstraction for the "outer" layers (userspace). It would be much weirder
for the outer layers to try to fix things up for the inner layers; that
abstraction goes the wrong way.

------
shin_lao
A tad off topic but worth pointing out nevertheless:

In C++ "exceptions should be exceptional", you don't need to fill you program
with try/catch blocks and shouldn't. Use return values instead (better
performance, flow less bug prone).

Never forget that an exception is essentially a goto statement.

In other words, you need rarely more than one try/catch block per thread.

~~~
grogs
Goto statements are not as horrific as comp sci 101 lectures suggest. This is
especially true when compilers do it, pretty sure JMP statements are used for
function calls.

~~~
Ralith
> This is especially true when compilers do it, pretty sure JMP statements are
> used for function calls.

That's a bit silly. It's like saying that writing raw opcodes is okay because
the assembler does it.

------
Someone
<http://en.wikipedia.org/wiki/Common_Lisp#Condition_system>

------
rubyrubyruby
Ruby (and a few other languages) have a 'retry' keyword, which retries the
block of code that had the begin/rescue around it (if this existed in C++ the
try block would be executed again).

begin processing() rescue retry end

In general this is a better solution because it forces a more structured style
of programming than opening up the scope of where the exception occurred and
allowing it to be manipulated. Opening up the scope would be more powerful,
but it would make the code harder to verify as being correct, and is open to
abuses.

~~~
lnanek
Hmm, I've had to do similar in Java before and it just took breaking out what
I wanted to retry into a separate method. Nice simple clean Ruby is preferred,
but it is possible already, at least...

------
mikmoila
Albeit about C++, Stroustrup's "D&E of the C++ programming language" has a
chapter which still is timeless and valuable source about exceptions and
fault-tolerance.

The model that all of a program is treated as "try" block is equivalent of
treating all code as part of fault-tolerance solution. As Stroustrup puts it
nicely, not all methods should be firewalls against errornous conditions, as
error handling in itself adds to the complexity of the program.

------
zurn
Any SmallTalk hackers care to summarise the high points of the way maybe
compared to the CL condition system? Wikipedia says they also have continuable
exceptions.

~~~
cwp
The above is more or less the way exceptions work in Smalltalk. (The
elimination of "try" doesn't apply, because Smalltalk has no keywords.)

• Not all exceptions are resumable; there's Error is a subclass of exception
that can't be resumed.

• Instead of conditions, Smalltalk allows a parameter to be supplied when
resuming an exception, and this will be answered by the #signal message that
originally raised the exception.

• Exception subclasses can implement #defaultAction, which is a used if there
are no catch blocks to handle the exception. The default implementation opens
a debugger.

The Wikipedia article on Lisp conditions (linked to by Someone) gives the
example of an exception raised when the program attempts to open a file that
doesn't exist. In Squeak (a dialect of Smalltalk) it would work like this,
given the following bit of code:

    
    
      1  [stream := FileDirectory default oldFileNamed: 'foo.txt']
      2    on: FileDoesNotExistException
      3    do: [:exception | 
      4        Transcript show: 'Could not open file: ', exception messageText.
      5        exception pass].
    

• the message #on:do: is sent to the block on line 1

• #on:do: sets an exception handler and then evaluates the block on line 1

• somewhere in the implementation of #oldFileNamed: an exception is raised

• the runtime scans the stack and finds the exception handler we installed

• it creates a new stack frame to evaluate the handler block (lines 3-5) and
passes in the exception as the argument

• line 4 creates a entry in the Transcript which is sort of a global log used
for debugging

• line 5 reraises the exception

• the runtime continues looking up the stack, but doesn't find another
exception hander

• the runtime sends #defaultAction to the exception object

• another frame gets added to the top of the stack to execute
FileDoesNotException>>defaultAction

• the default action opens a dialog saying the file doesn't exist, and offers
several options: • (a) create the file • (b) choose another filename • (c)
open a debugger

• if the user chooses (c), a debugger opens on the stack frame where the
exception was raised

• if the user chooses (a), the file is created and opened

• if the user choosed (b), they get to type in the new filename, and we try to
open that file, possibly raising a new exception

• if we successfully open the file via (b) or (c), the exception is resumed
with the stream as argument

• the runtime unwinds the two stack frames it created

• the activation of #oldFileName: resumes and answers the stream

• the stream gets assigned to the stream variable, and execution continues,
presumably to read the file

~~~
zurn
But what if you want to continue execution in the code block? Hmm,
<http://smalltalk.gnu.org/wiki/Exceptions> advertises something called #resume
for this.

------
neilebryant
"resume" is the VisualBASIC version. In an exception handler, you can use
'Resume Next' to jump back to the line following the error, or jump to a
label, etc. Many people would only be familiar with the 'On Error Resume Next'
version, which ignores errors.

This link has a good description, half-way down;
<http://www.cpearson.com/excel/errorhandling.htm>

------
deanfranks
I don't think this would work on the code or compiler end. For the compiler,
when an exception is thrown, the stack is unwound back to the most recent try
so the exception handler can be run in the scope of the try. In order to
implement this, the portion of the stack that is unwound would have to be
saved somewhere and when the recover is executed, the stack would have to be
unwound back to the try again, the saved portion of the stack would have to be
"repushed" and execution would resume.

On the code end, how would the exception code know what failed and what to
twiddle to fix things? The logic associated with the exception code would be
extremely complex and tightly coupled to the "downstream" code. It would make
much more sense to validate things before performing operations you know might
fail to reduce the chances that an exception would occur. Remember that
executing the throw portion of an exception is extremely expensive. Exceptions
should only be used for exceptional cases, not as a flow control mechanism for
a common execution path.

~~~
DrJokepu
I'm not super familiar with compiler internals, but wouldn't it be possible to
push the exception-handling frame as an additional frame to the top of the
stack instead of unwinding? This way the stack would be preserved. In
addition, if the local symbols are kept around and the optimising compiler
doesn't reuse stack space for multiple variables, wouldn't that allow binding
the symbols of the exception throwing stack frame to either local variables in
the exception handling scope or some sort of a dictionary?

~~~
deanfranks
It would be ugly. For many architectures, locals are referenced relative to a
[stack] frame pointer. The local variables from the original context of the
function containing the catch would need to accessible, and any additional
local variables allocated during the execution of the catch could not be added
to the area addressed by the frame pointer because that portion of the stack
would contain return addresses and locals for the function throwing the
exception, the exception data and any additional functions between the catch
function and the throw function.

If is far from impossible to implement, but it would be messy.

IMHO try/catch blocks for this kind of retry logic would have to be very small
in scope to be practical and maintainable. Any significant complexity in the
scope of the try block (multiple nested functions, etc) is going to create two
very tightly coupled sets of code non-obviously separated in the source. This
kind of pathological coupling is evil.

Since the scope should be kept small, I would always opt for comprehensive
sanity checking before the operation rather than complex, oddly coupled code
that is almost impossible to test.

------
dmethvin
Exception models have been analyzed extensively for about 40 years, what
you're referring to is the "resumption model". I know PL/I supported it, circa
1980. There are problem domains where it makes sense, think about hardware
exceptions for example where you generally can resume.

------
Codester
When to use exceptions is very simple.

When a function is unable to perform its task, throw an exception. It's that
simple. Do not use error codes to tell the caller "I cannot do what you
asked", throw an exception.

Here are some reasons:

1) Programmers pathologically ignore or forget to check return codes from
functions and now there are hard to find bugs in the program

2) A clean and beautiful algorithm is made obscure by messy error handling
code

3) Indecision and disagreement about how to deal with errors in a program:
throw exceptions

4) Return codes cannot provide enough information about an error in order to
decide how to handle it

5) Many functions cannot return error codes (Constructors, operator overloads,
conversion operators)

6) Return codes corrupt the meaning of the word "function" and destroy the
code's usability as a function

7) The need to signal an error condition in code that has no provision for
dealing with errors because when it was written no one expected that an error
could occur here

8) Functions tangled with return code checking, propagate their problems to
anyone else using them, spreading complexity like a cancer

9) Error handling code doesn't get tested

10) Consistent error handling with return codes triples your code size because
every function call will need "if fail then return fail code"

11) Errors are occurring in the program but where to look in tens of thousands
of lines of code? Exception constructors can log when, where and how telling
you exactly where to look.

~~~
philwelch
2 is a completely separate concern. It's best to do all your error handling at
once, at the beginning of the function, rather than mixing it in with your
algorithmic code. But that style has nothing to do with whether you throw
exceptions or return error codes; you can return error codes early, and you
can throw exceptions in the middle of your algorithmic code.

On the other side of things, if you're calling functions you expect to handle
errors, wrapping _that_ up in a try/catch block is no less messy than checking
the validity of your objects as you receive them from those function calls.

------
huggah
This is close to how Go handles exceptions. The 'defer' statement schedules a
function to be executed at the end of the current function, no matter now the
current function is exited. The recover() call returns an active panic (panic
being Go for exception)---it is only useful inside a deferred function.

See a fuller and better explanation at <http://blog.golang.org/2010/08/defer-
panic-and-recover.html>

~~~
pcwalton
No, it's not. Go handles errors via exceptions (which Go calls panic and
recover) and return codes (and "defer" is just "finally"). In other words, Go
has a mix of C-style and Java-style error handling; it doesn't have any more
features than either of them. (This is of course totally fine; Java's
exception system is not a bad exception system.)

What the post is asking for is more like Common Lisp's condition system.

~~~
huggah
> Whenever an exception happens, programmers could twiddle a few variables and
> set the program back to where it was before.

This is missing. The first point of the OP is entirely as described though.

------
anuraj
There are few concerns: 1) It is likely that the exception is non recoverable,
from the place it occurred and may need to be wound back multiple levels

2) Runtime that defensively handle exceptions as specified at every point will
need to keep stack state indefinitely deep

I believe, rather than exception syntax, the issue is abuse of exceptions.
Exceptions must be raised on recoverable errors, but they must be handled at
the right abstraction level. So how about raising the 'throws' on your
functions and then handle them at the right abstraction level. Why not spent a
little time designing your exception handling tree before implementing.

That said, the 'recover' keyword may be really helpful in many cases.

------
EdgarVerona
Hmm... you know, in a way, VB6 had a similar model, at least with the
"recover" keyword. I think it was something like "ON ERROR RESUME NEXT".

It was pretty terrible, but mostly because it involved GOTOs and labels rather
than actual blocks.

------
Layke1123
Brilliant!

