It seems that these days whenever anyone describes some language feature, I’m compelled to compare/contrast that with how we’ve done things in the new neptune language. Most recently James Robertson has written about exceptions in smalltalk so, well, here we go.
In smalltalk exceptions capture the stack when they’re thrown which is a bit similar to the way stack traces are captured by exceptions in java. In java, though, what is stored in the exception is exactly a stack trace which is a serialized copy of the real stack that you can print and… well, not much else. In smalltalk the exception actually captures the real live stack which means that execution can be resumed from wherever the exception was thrown. This borders on what MenTaLguY calls “the Lovecraftian madness of full continuations”. But even though this model is neat and general, everyone I’ve ever talked with about resumable exceptions, at least those that have used them in practice, have told that their usefulness is actually pretty limited. James’ example code does little to change my attitude; that code is so convoluted that I take it more as an argument against than for resumable exceptions.
My attitude against resumable exceptions may just be a convenient rationalization, though. On the OSVM platform, which must be able to run on platforms with very little memory and processing power, we can’t afford the full smalltalk model anyway. In the previous incarnation of OSVM, which was based on smalltalk, there were no stacks or stack traces. You could throw any object as an exception and if someone else caught it there was no way to tell where it had been thrown from. In addition, there was no exception hierarchy and we usually just threw text strings or symbols: calling a method that hadn’t been defined caused the system to simply throw the string "LookupError"
with no further information about which method was called on which object. Clearly there was room for improvement and now that we’ve had an opportunity to revisit the design, we’ve improved it quite a bit. The biggest change we’ve made has to do with, you guessed it, the stack. But that aspect will have to wait a bit.
The old system allowed you to throw any object so there was really no technical reason not to have an exception hierarchy and throw structured and informative exceptions rather than flat text strings. We just never really got around to it. For the new language we were rewriting all our libraries anyway so we added structured exceptions as soon as we had implemented exception handling. And, I must confess, doing it early on also meant that it would be a major hassle to take them out later if it turned out that someone thought it was a bad idea. Did I mention that the code name of our next release is “sneaky monkey”? Anyway, now when an error occurs somewhere because you’re trying to reverse a string (which you can’t currently do) you’ll an error that tells you exactly that: LookupError("ksiftseh".reverse())
.
In neptune, the syntax for exception handlers is somewhat similar to C++, Java and C#. Here’s how an exception handler looks in C++:
try {
...
} catch (exception& e) {
...
}
This exception handler catches any objects thrown that descend from the class exception
, because that’s the declared type of the exception variable e. Java and C# use the same approach. In neptune we have to use a slightly different model because we have optional typing, which means that type declarations are not allowed to affect the runtime semantics of a program. Here’s an example of a neptune exception handler:
try {
...
} catch (Exception e: IOException) {
...
}
The exceptions handled by this catch clause are listed after the colon, in this case IOException, and the declared type of the variable, Exception, has no influence on which exceptions are caught. One thing this means is that you can now catch several unrelated exceptions with the same exception handler:
try {
...
} catch (Exception e: IOException, AssertionError) {
...
}
If the variable has the same type as the exception being caught, which is usually the case, you can just leave out the type declaration:
try {
...
} catch (e: IOException) {
...
}
The try/catch syntax is not “magic” but a shorthand that, like all our other shorthands, expands straightforwardly into simple method calls. A try statement corresponds to a call to the try_catch method on Function. The handler above expands into something like this
fun {
...
}.try_catch(new Vector {
IOException.protocol,
AssertionError.protocol
}, fun (Exception e) {
...
})
Hey I didn’t say that the result was straightforward, just that the transformation was. And even though it may look expensive, with allocations and whatnot, the generated code is very efficient no matter if you use the try/catch syntax or write the call to try_catch
yourself. An explanation of what .protocol
means will have to wait.
Now for the stack traces. As you’ll remember, in Java and smalltalk the stack is stored in the exception object being thrown. I’m not one to underestimate the ingenuity of the people that implement smalltalk or java (since a majority of my colleagues have done both) but no matter how clever you are, storing the stack in an exception is bound to cost space and time. The worst part is that when creating or throwing an exception in smalltalk or java, you don’t actually know if the stack will be used or not so you always have to store it, just in case. That won’t do in OSVM because we don’t have processing power or space enough to do stuff “just in case”. Instead, you can specify that you want a stack trace at the location where you’ll be using it: the exception handler:
try {
...
} catch (e: IOException) at (StackTrace s) {
...
}
This way the stack trace and the exception object are completely decoupled and we only need to instantiate the trace when it is really needed. Another advantage of decoupling the trace is that throwing an object doesn’t change it. In smalltalk, throwing an exception that has been thrown before causes the previous stack to be overwritten, which is not an especially clean model. In Java the stack trace stored in the exception is captured when the exception is instantiated, not when it is thrown. In my experience that is the desired semantics only in one situation: if you want to get the stack trace but don’t want to throw an exception. Doing
new Throwable().printStackTrace()
is not uncommon in java but it’s a hack really. In neptune there’s a much cleaner way to get the current stack: just call the static Thread.current_stack()
method.
I don’t know of any other language with a similar model but my experience so far is that it is exactly what you want and, in addition, simpler to implement and less expensive than the alternatives. There are still issues to work out, like what happens when you rethrow an exception and what exactly should be stored in a stack trace object. The biggest problem for me, though, is the keyword at. This is something we can still easily change so if you have a good alternative let me know.
2 Responses to Exceptions