DIP33

From D Wiki
Revision as of 08:48, 1 April 2013 by Kyllingstad (talk | contribs)
Jump to: navigation, search

This DIP is currently under construction. Please come back later.

Title: A new exception hierarchy
DIP: 33
Version: 1
Status: Draft
Created: 2013-04-01
Last Modified: 2013-04-01
Author: Lars T. Kyllingstad
Links:

Abstract

This is a proposal for a new exception hierarchy for Druntime and Phobos.

Rationale

Overview

The following is an outline of the exceptions in the hierarchy, and how they are related to each other. Deeper levels are subclasses of those above.

  • Throwable
    • Error
      • AssertError
      • FormatError
      • InvalidArgumentError
      • RangeError
    • Exception
      • ConversionException
      • EncodingException
      • FilesystemException
      • IOException
        • NetworkException
      • ParseException
        • DocParseException
      • ProcessException
      • SystemException
        • ErrnoException
        • WinAPIException
      • ThreadException
    • OutOfMemory


Top-level classes

Strictly speaking, Throwable is of course the (only) top-level exception class. Here, however, we discuss its direct descendants, from which all other exception classes derive.

Error

class Error : Throwable { }

Error and its subclasses are used to signal programming errors. If an Error is thrown, it means that there is something wrong with how the program is constructed. Examples include array index out of bounds, invalid function arguments, etc. Importantly, it should always be possible to avoid an Error by design.

In general, Errors should not be caught, primarily because they indicate that the program logic is compromised, and that the program may therefore be in an invalid state from which there is no recovery. Furthermore, one cannot rely on them being thrown at all. For example, assert statements and array bounds checks, which both trigger Errors, may be disabled by compiler switches.

If an Error must be caught, it is recommended to do so at a very high level (e.g. in main()), and then only to perform critical cleanup work before terminating the program gracefully.

Exception

class Exception : Throwable { }

Exception and its descendants are used to signal normal runtime errors. These are exceptional circumstances that the programmer cannot be expected to avoid by design. Examples include file not found, problems with parsing a document, system errors, etc.

OutOfMemory

class OutOfMemory : Throwable { }

This exception is thrown on an attempt to allocate more memory than what is currently available for the program. Strictly speaking, this is not an Error, as the programmer cannot reasonably be expected to check memory availability before each allocation. However, is not desirable to catch it along with normal Exceptions either, as an out-of-memory condition requires special treatment. Therefore, this DIP places OutOfMemory at the top level of the hierarchy, alongside Error and Exception.

Errors

Here follows a more detailed description of the various Error subclasses.

AssertError

class AssertError : Error { }

This error is thrown when an assert statement fails.

FormatError

class FormatError : Error { }

This error is thrown by functions such as std.format.formattedWrite(), std.stdio.writeln(), and so on, to signal a mismatch between format specifiers and the provided objects.

Supersedes: std.format.FormatException

InvalidArgumentError

class InvalidArgumentError : Error { }

This error is thrown when one or more function arguments are invalid. Since it is an Error, it should only be used to signal errors that the programmer (i.e. the user of the function in question) can reasonably be expected to avoid, and which are not too costly to check. Circumstances that are out of the programmer's control, or which are so expensive to verify that it is undesirable to have them checked by both the caller and the callee, should be signalled with an Exception instead.

void processFile(string path)
{
    // The following is an acceptable use of InvalidArgumentError,
    // as the function should never be given an empty path, and the
    // check is trivial.
    if (path.empty)
        throw new InvalidArgumentError("path is empty");

    // The function caller should not be expected to verify file existence.
    // Firstly, it could change between the time it is checked and the time
    // the function is called, and secondly, it requires filesystem lookup
    // which is a relatively expensive operation.
    if (!exists(path))
        throw new FilesystemException("File not found: "~path);
}

RangeError

class RangeError : Error { }

This error is thrown on illegal range operations. Examples include when an array index is out of bounds, when front or popFront() is called on an empty range, etc.

struct MyRange(T)
{
    @property bool empty() { ... }
    @property T front()
    {
        if (empty) throw new RangeError("front called on empty range");
        ...
    }
    void popFront()
    {
        if (empty) throw new RangeError("popFront() called on empty range");
        ...
    }
    ...
}

Exceptions

ConversionException

class ConversionException : Exception
{
    /// Different kinds of conversion errors.
    enum Kind
    {
        invalid,
        overflow,
        underflow
    }

    /// Which kind of conversion exception we are dealing with.
    Kind kind;
}

This exception is thrown on failure to convert one value/type to another. Its most prominent use will of course be in std.conv, but it is by no means limited to this module.

Supersedes: std.conv.ConvException, std.conv.ConvOverflowException

EncodingException

class EncodingException : Exception { }

This exception is thrown when an error is detected in a low-level data encoding. Normally, this will be binary encodings such as UTF, Base64, various compressed data formats, etc.

Supersedes: core.exception.UnicodeException, std.base64.Base64Exception, std.encoding.EncodingException, std.encoding.UnrecognizedEncodingException, std.utf.UTFException, to some extent std.zip.ZipException

See also: #ParseException, DocParseException

FilesystemException

class FilesystemException : Exception
{
    /// The various kinds of filesystem errors.
    enum Kind
    {
        unknown,
        fileNotFound,
        permissionDenied,
        fileExists,
        invalidFilename,
        notAFile,
        notADirectory,
    }

    /** The path to the filesystem node with which there was a problem,
        or null if the exception is not associated with a particular node.
    */
    string path;

    /// Which kind of error we are dealing with.
    Kind kind;
}

This exception is thrown on errors that occur during filesystem operations such as file lookup/deletion/renaming, directory change, etc.

ParseException, DocParseException

class ParseException : Exception { }

class DocParseException : ParseException
{
    /** The path to the file in which the error was detected, or null if
        the exception is not associated with a disk file.
    */
    string file;

    /** The line number at which the error was detected, or 0 if the exception
        is not associated with a particular line.
    */
    uint line;

    /** The column number at which the error was detected, or 0 if the exception
        is not associated with a particular column.
    */
    uint column;
}

These exceptions are thrown on errors that are detected while parsing a high-level file or data format. Typical examples are markup languages (XML, JSON, etc.), programming languages, high level data containers (ZIP, OGG, etc.). Use DocParseException for human-readable formats where the error can be traced back to a specific file, line and/or column.

See also: #EncodingException

Exceptions from C errors

The D standard libraries make heavy use of C APIs under the hood. These typically signal errors by means of an errno code, or, in the case of the Windows API, a code returned by GetLastError(). As such, the exceptions described in this DIP will often be associated with such an error code, and it may sometimes be useful for the programmer to be able to access it.

Today, many exception classes have this functionality built in, such as std.file.FileException, std.socket.SocketOSException, std.stdio.StdioException, etc. There are a few problems with this:

  1. The same error-code handling functionality is duplicated across several classes.
  2. These classes may also signal errors which are not associated with a system error code, and in this case the presence of such functionality may be confusing.

Another approach which is also used in Phobos is to throw an std.exception.ErrnoException. This makes it obvious that the error is associated with an errno code, but it does not statically classify the error. It could be a filesystem error, a read/write error, a process creation error, or, basically, anything. This defeats the purpose of having an exception hierarchy in the first place.

This DIP therefore proposes that the standard way to handle this situation should be to create a separate exception for the system error, in the form of a #SystemException or one of its descendants, and to chain this exception to a higher-level exception. Here is an example:

struct MyFile
{
    this(string path, string mode)
    {
        try
        {
            myOpen(path, mode);
        }
        catch (ErrnoException ex)
        {
            switch (ex.errno)
            {
            case EINVAL: throw new InvalidArgumentError("Invalid file mode: "~mode, ex);
            case EACCES: throw new FilesystemException(path, FilesystemException.Kind.permissionDenied, ex);
            case EEXIST: throw new FilesystemException(path, FilesystemException.Kind.fileExists, ex);
            // ...and so on.
            }
        }
    }

private:
    void myOpen(string path, string mode)
    {
        m_file = fopen(toStringz(path), toStringz(mode));
        if (!m_file) throw new ErrnoException; // errno is automatically picked up
    }

    FILE* m_file;
}

Copyright

This document has been placed in the Public Domain.