Error handling

D includes support for exception-based error handling. Another option is a popular feature called the scope statement.

Scope guards

In a C function that manipulates a locally allocated buffer, it's not unusual to see a series of if…else blocks where, after the failure of some operation, either the buffer is freed directly or via a goto statement. In D, we need neither idiom:

void manipulateData() {
  import core.stdc.stdlib : malloc, free;
  auto buf = cast(ubyte*)malloc(1024);
  scope(exit) if(buf) free(buf);
  // Now do some work with buf
}

Here, memory is allocated outside the GC with malloc and should be released when the function exits. The highlighted scope(exit) allows that. Scope statements are executed at the end of any scope in which they are declared, be it a function body, a loop body, or any block scope. exit says the statement should always be executed when the scope exits. There are two other possible identifiers to use here: success means to execute only when the scope exits normally; failure executes only after an uncaught exception. Braces were not used here since this is a one-liner but, as with any other block statement, they can be. Multiple scope guard blocks can be declared in any scope. They are executed in the reverse order of declaration.

Exception handling

DRuntime declares three globally accessible classes that form the bedrock of the exception mechanism: Throwable, Error, and Exception. The latter two are both subclasses of the first. Instances of the Error class are intended to represent unrecoverable errors, while the Exception class represents errors are potentially recoverable. I'll refer to both as exceptions throughout the book except when I need to talk specifically about the types.

Exceptions can be thrown and caught. Only instances of classes in the Throwable hierarchy can be thrown, including subclasses of Error and Exception. To throw an exception, use the throw statement:

throw new Exception("Very bad things have happened here.")

The text given to the constructor is accessible via the .msg property. The toString function of an exception includes the message, along with the fully qualified class name of the instance and a backtrace showing where the exception occurred. Normally, the backtrace only shows memory addresses but if the source is compiled with the -g option, a more human-readable backtrace is generated.

To catch an exception, use the standard try followed by one or more catch blocks, a finally block, or both. Each has its own scope and optional braces:

void main() {
    import std.exception : ErrnoException;
    try {
        auto file = File("log.txt", "w");
        file.writeln("Hello, file!");
    }
    catch(ErrnoException ee) {
        // Do something specific to the ErrnoException
        writeln(ee);
    }
    catch(Exception e) {
        // Do something specific to the Exception
        writeln(e);
    }
    finally
        writeln("Good bye!");
}

Since file is declared in the try block, it is not visible in either the catch or finally blocks. The catch blocks are only run when the corresponding exception is thrown inside the try, while the finally is always run when the try block exits. Scope statements are syntactic sugar for try…catch blocks (and scope(exit) adds a finally). With multiple catch blocks, any subclasses having super classes in the chain should be declared first. Since ErrnoException is a subclass of Exception, it has to come first; if Exception were first, ErrnoException would never be caught.

Note

When you find yourself wanting to catch specific exceptions, the Phobos documentation lists the exceptions a function may throw (they aren't listed in function declarations as in Java; there are no checked exceptions in D). It does so via the Throws: field in Ddoc comments. When releasing your own APIs to the public, you should do the same for your documentation. For our previous example, you can look at the documentation for std.stdio.File at http://dlang.org/phobos/std_stdio.html#.File to see which functions throw which exceptions.

Exceptions that are not caught will filter up the call stack and ultimately cause the program to exit, with the result of its toString implementation printed to the console. Generally, you should only catch Exception or its subclasses. When Exception is thrown, things that normally run as a scope are exited, such as struct destructors and scope statements, are still guaranteed to run; there is no such guarantee when Error is thrown, meaning the program could possibly be in an invalid state by the time the catch block runs. As a general rule, never catch Throwable (you can't know whether it's an Error or Exception until after it's caught) and only catch Error if you really know what you are doing. When you do, rethrow it as soon as you're finished with it. A program should never attempt to recover when Error is thrown.

When a function is annotated with nothrow, the compiler requires that any function it calls also be marked nothrow. Exceptions cannot be thrown from inside such a function, but Errors are still allowed. Try to compile this:

void saveText(string text) nothrow {
  auto file = File("text.txt", "w");
  file.writeln(text);
}

You should see four errors. Three of them tell you that the constructor and destructor of the File type, as well as its writeln method, are not marked nothrow. The last one says that saveText is marked nothrow even though it may throw. In order to live up to the promise of nothrow, the function body should be wrapped up in a try…catch block:

try {
  auto file = File("text.txt", "w");
  file.writeln(text);
}
catch(Exception e) { /* Log the message */ }

Exception and Error can be subclassed to create custom exceptions. When doing so, at least one of the super class constructors must be called from the subclass. Exception has constructors with the following signatures:

this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null)
this(string msg, Throwable next, string file = __FILE__, size_t line = __LINE__)

The next parameter is used to set up a chain of exceptions, but is primarily used internally. Most notable here are the __FILE__ and __LINE__ identifiers. C and C++ programmers will reflexively think of preprocessor macros that have the same name. If you happen to be one of them, please push that thought from your mind right now. The purpose of these identifiers is the same as the C macros, but they are implemented as special constants that the compiler substitutes directly where encountered. Moreover, using the macros as default values in C++ would cause the line number and filename of the function declaration to be inserted. In D, it's the line number and filename of the call site. When extending Exception (Error constructors don't have the same signatures), be sure to add the same parameters to your own constructor(s) and pass the values on to the super class constructor. The feature is useful elsewhere, particularly for custom logging functions intended to log the file name and line number of the caller.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.220.16