What are Exception
s and Error
s in D? Why is there a difference? Why does D consider Error
s throwable inside a nothrow
function? Sometimes decisions seem arbitrary, but when you finally understand the reasoning, you can better appreciate why things are the way they are.
Throwing a Throwable
I’m not going to go into all the details of how throwing works in D or in any other language (that is easy to find online). But simply put, an exception is a “exceptional” case, which shouldn’t occur in normal code. The reason throwing is preferred to other types of error handling (such as returning an error code, or a combination error/value) is because an exception requires handling. If you don’t handle it, someone else will. And the default is to print out most of the state that was happening when the exception happened, and exit the program.
As a side note, the most recent compiler has a new feature called @mustuse
which requires that any return value (which might contain an error) must be dealt with.
That being said, throwing is relatively expensive, meaning that you should only do it in truly exceptional cases, and not use it for mundane flow control.
In D, you throw an exception or error simply by using the throw
statement, which requires an object instance that is a derivative of Throwable
:
int div(int x, int y) {
if(y == 0) throw new Exception("Divide by zero!");
return x / y;
}
Then you can catch it somewhere else. When you catch it, the exception contains all the information on how it was generated, including the file/line that generated the exception and all the places along the call stack that got to that point. The beauty of exception handling is you can put the handler anywhere along the call stack — where it’s most needed.
Consider a web server, where you might want an exception handler at the part that handles the HTTP request, where you can return the appropriate HTTP error code, and maybe a nice page sent back to the user. Instead of having to propagate some error deep in your page handler code up the call stack so it can be properly handled, you just throw the exception where it happens, and catch it where you want to handle it. The language takes care of the rest!
Stack Unwinding
One of the bookkeeping tasks that the language has to deal with is unwinding the stack. If for instance you have structs on the stack with destructors, those destructors have to be called, or else your program integrity is compromised. Imagine if a reference-counted smart pointer didn’t decrement its reference when an exception is thrown. Or a mutex is left locked.
There’s also scope-guard statements which help properly design initialization/cleanup code without having to remember cleanup at the end of scopes, or in every spot where a return statement exists. Those must also be run when an exception is thrown.
nothrow functions
A nothrow
function is one that cannot let Exceptions escape handling outside the function. That means you must handle all possible exceptions that might be thrown inside your function or inside any throwing function you call. A nothrow
function’s purpose is to inform the compiler that it can omit cleanup code for exception throwing.
This allows the compiler to both output less code, and also gives the optimizer more possibilities to work with, making nothrow
functions preferable to ones that throw.
Stack Unwinding for Errors
However, a nothrow
function is still allowed to throw an Error
. How does that work?
How it works is that the compiler still omits exception cleanup code, and the code that catches the Error
is not allowed to continue the program. If it does, the program may obviously be in an invalid state. You can think of the throw and catch of an Error
to be like a plain goto
instruction.
The following code example and output demonstrates how cleanup code is skipped:
// For example:
void foo() nothrow {
throw new Error("catch me!");
}
void bar() nothrow {
import core.stdc.stdio;
scope(exit) printf("cleaning up...\n");
foo();
}
void main() {
bar();
}
object.Error@(0): catch me!
----------------
./onlineapp.d:3 nothrow void onlineapp.foo() [0x55db91086345]
./onlineapp.d:9 nothrow void onlineapp.bar() [0x55db91086350]
./onlineapp.d:13 _Dmain [0x55db9108636c]
It can be tempting to catch an Error
and use that as a control flow mechanism. For example, an array out of bounds access is a frequent error that you may want to just recover from. But the stack frames may not be properly cleaned up, which means things like mutex unlocks, or reference decrements didn’t happen along the way up the stack.
In short, your program is in an undetermined state. Continuing execution risks damaging the data used by the program, or crashing the user’s application.
How to handle Errors
Don’t. The only exception (pun intended) is when you are testing code. And actually the language guarantees proper stack unwinding for assert errors thrown inside unittests and contracts.
As a rule of thumb, an Error
is for programming errors (that is, conditions you expect to be enforced by the programmer are incorrect), and an Exception
is for environment/user errors.
If you do catch an Error
, the only proper action is to perform some possible final action (such as logging the error) and exiting the program. And make sure any final actions you perform can’t be thwarted by undetermined state.
Edit: More Pitfalls!
After much discussion on the D forum, one user (frame) noted that you can return from a scope(failure)
statement.
I didn’t go over exactly what a scope guard statement was, but essentially there are 3 conditions that you can use to run cleanup code, exit
, success
, and failure
. I used the scope(exit)
code above to show an example of skipping cleanup code.
A scope(failure)
statement executes when a function is exiting because a Throwable
is thrown. However, an Error
is a derivative of Throwable
, so this includes Error! Normally, this isn’t a problem, because after the statement is done, the code normally just rethrows the Throwable
. However, you are allowed (per the spec) to simply return normally, use goto
to exit the statement, or throw an Exception
. Any of these mechanisms will mask the fact that an Error was thrown, and that the program is now in a possibly invalid state.
I recommend at this point NOT to use these mechanisms, and I have advocated on an existing dlang issue that the language revoke this allowance.
So what if you want to return a code if an Exception
is thrown? Well, the compiler actually rewrites a scope(failure)
statement like:
// scope(failure) <code>; // is rewritten as
try {
... // all code after the scope(failure) statement
} catch(Throwable _caught) {
<code>
throw _caught;
}
Instead, you can expand the statement and change the Throwable
to an Exception
to make sure you aren’t inadvertently masking an Error
from propagating:
try {
... // normal function code
} catch(Exception) {
return 10;
}