Hacker News new | past | comments | ask | show | jobs | submit login
Rethinking try/catch
23 points by Tangaroa on May 5, 2012 | hide | past | favorite | 35 comments
Consider these changes to the try/catch model of C++ and Java:

* 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.

* 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.

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?




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)


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.


Using return values to pass back error codes is a disaster waiting to happen that can triple your line count.


Please explain.


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.


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



By my count, you have made five substantially different claims there: in C++, exceptions should be exceptional; you rarely need more than one try/catch per thread; return values give better performance; return values make the flow less bug prone; and an exception is essentially a goto statement.

I challenge every one of those five claims. Can you provide a reasoned argument and/or empirical data to support them?


Exception performance is pretty obvious: you need to unwind the stack everytime you catch an exception whereas checking an error return value is one instruction long.

If you try to catch your exception at every statement, you're not using RAII correctly.

Returns values are less bug prone because behaviour is obvious (boost::system::error_code is a good way to encapsulate information).

Exception is a goto statement because basically you're breaking the flow and saying "go to catch in case of error".

For all these reasons, exceptions should be exceptional.

Use exceptions for abnormal behavior that you cannot properly handle in the program (out of memory, i/o error...).


Exception performance is pretty obvious: you need to unwind the stack everytime you catch an exception whereas checking an error return value is one instruction long.

It's not obvious at all. You need to unwind the stack anyway. If you do so using some form of jump table when an exception is thrown, you only have a single step to deal with instead of a whole series of returns. Moreover, on the non-exceptional path, you no longer need check-and-branch logic after the return from every function that might fail.

If you try to catch your exception at every statement, you're not using RAII correctly.

Sure, but that doesn't imply your conclusion that usually you would only want one try/catch per thread. Why can't we catch an exception at whatever level of our code knows how to respond to that situation? Why would we assume that the only levels of logic in our code are low-level work that might throw and a top-level oversight loop?

Returns values are less bug prone because behaviour is obvious (boost::system::error_code is a good way to encapsulate information).

C++ does non-obvious things all the time, and that can be helpful because it removes unnecessary details so that we can concentrate on the logic that matters. Would you also argue that using RIAA is bad because if we explicitly released all our resources before every possible return from the function it would make the behaviour obvious?

Exception is a goto statement because basically you're breaking the flow and saying "go to catch in case of error".

Except that you're not just saying "goto catch", are you? Like return, continue and break statements, exceptions can only transfer control to another location in a structured way, in this case, the equivalent of returning early from the lower-level functions and going back up the call stack. You can't escape the structured programming roots of the code using an exception the way you can with goto, and the semantics of unwinding the stack when an exception is thrown are well defined and widely understood.

For all these reasons, exceptions should be exceptional.

Use exceptions for abnormal behavior that you cannot properly handle in the program (out of memory, i/o error...).

Sure, that's one useful role for exceptions, but why should we limit our use of a tool based on what the tool happens to be called? I don't think they had modern C++ libraries and metaprogramming in mind when they added templates to the language.


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.



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.


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


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.


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.


How about continuable exceptions with a Catch block that just applies to anything that follows in the current scope? Better yet, have this be implemented as a system-level function, so one could store a block of code or an object implementing the correct protocol and have code reuse for exception handling. (In Smalltalk, you have access to the stack frame where the exception was thrown from the exception itself.)

At least one Smalltalk had a "top level" exception handler that just acted as the handler of last resort for everything currently running. (And it should be possible to construct one for any Smalltalk whose event dispatch is implemented as Smalltalk.)


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


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.


"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


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.


  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.
Which is precisely what continuations do. Not exactly an unsolved problem, and pretty much the reason why you can implement this in Lisp with call/cc.

  On the code end, how would the exception code know
  what failed and what to twiddle to fix things?
That's a more interesting question, but it's at least partially answered by "what does a normal catch block do?" Presumably, exceptions contain some information about what they are and why they happened.


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?


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.


As long as you keep two stack pointers (one to the point where the exception was thrown, and then unwind the second to the try block) it shouldn't be that much of a problem. Just specify that you can either recover (at which point you change the state from where the exception is thrown and abandon the stack pointer at the try block) or catch (at which point you abandon the stack pointer at the throw statement and are free to call functions, etc. to otherwise recover from the exception) but not both.


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.


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


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.


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


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.


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.


Design by Contract - types of preconditions and exceptions is very powerfull tooling for dealing with situations not expected to happen during coding.

I would still to keep error handling and detection as separate issues. E.g merely throwing and exception or letting it to happen is often merely error detection, and handling is still to be done somewhere else.

I think that exception systems merits are in keeping the subsystems decoupled by giving clear and systematic signaling channel to be used in error-detection situations.


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.


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.


Brilliant!




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: