D from the ground up

Classes and exceptions

The one place this course uses a class, how throw moves control to a catch, and how to give a failure its own type and message.

Part 10 of 12

Advanced19 min readBack to Courses

A class is a reference type

class ScoreError { ... } declares a type whose values live on the heap and are reached through a reference. Creating one takes new, and assigning one class variable to another copies the reference rather than the object, so both names reach the same value. Structs, the previous chapter, are copied whole instead.

A class can extend another with a colon: class ScoreError : Exception means every ScoreError is also an Exception. This course keeps classes to this one job, because a record is better modelled by a struct — but the error types the language itself throws are classes, so a custom error is where a class earns its place.

throw, try and catch

throw new ScoreError("score out of range") stops the current path and starts looking for a handler. The call stack unwinds, function by function, until a try block with a matching catch is found. If nothing catches it the program stops and prints the error, which is at least honest behaviour.

try marks the code that might fail and catch (ScoreError e) names the type it can handle. The caught value carries its message in e.msg, the string that was passed to the parent class when the error was built. After the catch block the program carries on as if nothing had happened, which is the advantage over crashing.

Catch what you can handle

Catches are tried in order, so a specific type belongs before a general one: catch (ScoreError e) first, then catch (Exception e) for anything else. Reversing the order is a compile error — the compiler reports that the first catch hides the second — so the mistake never reaches a running program.

A rule that cannot be met is the right reason to throw: a score outside 0 to 100, a missing key that the function promised to find, a file that is not there. A rule that the caller could simply have checked twice is a poor reason, and a failure used for ordinary control flow is worse.

A catch that does nothing is worse than no catch at all: the program continues with a value it never computed, and the failure surfaces later at a line that had nothing to do with the cause. If a catch cannot fix the problem, it should rethrow with throw e; so an outer handler can.

Unwinding, and the code that must still run

When a throw leaves a function, the runtime destroys that frame and keeps unwinding until a matching catch is found. Cleanup written after the throw would never run, which is why D gives you scope(exit): the block runs when control leaves the scope by any route, including a throw, a return or the ordinary end of the block.

finally is the same idea attached to a try. It runs whether the try finished, threw or returned early. Several scope blocks run in reverse order, like a stack. Use scope(exit) for one statement of cleanup and finally when the cleanup needs its own block.

Exception or Error?

D splits throwable values into two families under Throwable. Exception means something the program can recover from: bad input, a missing key, a rule the caller broke. Error means the program itself is wrong: an out-of-bounds index, a failed assertion, a null dereference. Catching Exception is normal; catching Error hides a bug and leaves the program in a state nobody planned for.

A function marked nothrow promises never to let an exception escape. The compiler enforces it, so a throw inside one has to be caught there. That attribute is how a caller knows it can skip the try.

The names are conventional but the split is real: the runtime throws an Error for a broken invariant, and no amount of catching makes the index valid. Treat an Error as a crash you want to see in development, not as a branch to handle in production.

Throw, catch, carry on

import std.stdio;

void fail(string why) {
    throw new Exception(why);
}

void main() {
    try {
        fail("no stock");
    } catch (Exception e) {
        writeln("error: ", e.msg);
    }
}

A score that cannot be right

Run it, then change the second call to check(70) and confirm that the program prints two ok lines and no catch. Then throw a plain Exception from somewhere in main and catch it in the same block.

Output
Click "Run" to execute your code

The line after the throw never runs. In the example the second call to check throws, so the writeln on that line is skipped and control lands in the catch block instead.

A rethrow, two cleanups, one catch

Run it and follow the order of the six lines. Then add a second `scope(exit)` at the top of `main` and predict which one runs first.

Output
Click "Run" to execute your code

Three mistakes: catching Exception with an empty block, which turns a failure into silence; putting the general catch before the specific one, which the compiler refuses because the first would hide the second; and throwing from a nothrow function without catching it there, which is a compile error rather than a surprise at run time.

Try these next

  • Add a second scope(exit) at the top of main and predict which of the two runs first.
  • Change risky to catch without rethrowing and check that the outer catch is skipped.
  • Throw a custom exception from a nothrow function and read the compiler message before removing the attribute.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does throw do?