6 ms·
Rethinking try/catch
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?
- Layke1123 14y agoBrilliant!
- zurn 14y agoAny 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.
- stcredzero 14y agoHow 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.)
- cwp 14y agoThe 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 14y agoBut what if you want to continue execution in the code block? Hmm, http://smalltalk.gnu.org/wiki/Exceptions http://smalltalk.gnu.org/wiki/Exceptions advertises something called #resume for this.
- deanfranks 14y agoI 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.
- gliese1337 14y agoIn 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.
- DrJokepu 14y agoI'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?
- Someone 14y agohttp://en.wikipedia.org/wiki/Common_Lisp#Condition_system http://en.wikipedia.org/wiki/Common_Lisp#Condition_system
- beagle3 14y agoCommon 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)
- deleted 14y ago[deleted]
- neilebryant 14y ago"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 http://www.cpearson.com/excel/errorhandling.htm
- huggah 14y agoThis 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 http://blog.golang.org/2010/08/defer-panic-and-recover.html
- pcwalton 14y agoNo, 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 14y ago> 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.
- dmethvin 14y agoException 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.
- shin_lao 14y agoA 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.
- PaulHoule 14y agoUsing return values to pass back error codes is a disaster waiting to happen that can triple your line count.
- shin_lao 14y agoPlease explain.
- grogs 14y agoGoto 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 14y ago> 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.
- zedadex 14y agoNo. http://en.wikipedia.org/wiki/Jump_to_subroutine#Jump_to_subroutine http://en.wikipedia.org/wiki/Jump_to_subroutine#Jump_to_subr...
- Silhouette 14y agoBy 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?
- rubyrubyruby 14y agoRuby (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 14y agoHmm, 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 14y agoAlbeit 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.
- apenwarr 14y agoI 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.
- anuraj 14y agoThere 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.
- Codester 14y agoWhen 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 14y ago2 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.
- mikmoila 14y agoDesign 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.
- EdgarVerona 14y agoHmm... 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.