Rethinking try/catch
* 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?
36 comments
[ 5.1 ms ] story [ 103 ms ] threadOn 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.
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.
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.)
• 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:
• 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
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)
This link has a good description, half-way down; http://www.cpearson.com/excel/errorhandling.htm
See a fuller and better explanation at http://blog.golang.org/2010/08/defer-panic-and-recover.html
What the post is asking for is more like Common Lisp's condition system.
This is missing. The first point of the OP is entirely as described though.
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.
That's a bit silly. It's like saying that writing raw opcodes is okay because the assembler does it.
http://en.wikipedia.org/wiki/Jump_to_subroutine#Jump_to_subr...
I challenge every one of those five claims. Can you provide a reasoned argument and/or empirical data to support them?
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...).
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.
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.
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.
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.
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.
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.
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.
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.
It was pretty terrible, but mostly because it involved GOTOs and labels rather than actual blocks.