Errors, Failures and Exceptions

Working without Null

Limpid does not use pointers, so that null return values are not possible. The solution to this difficulty is to use isValid() for nodes and empty() or size() or getLength() for strings. It should be understood that the return of null (or a failNode, etc, in Limpid) is a failure, and not an error. Failures are part of the normal routine of operation and must be allowed for in code. Now, if we were working with pointers and had a null return value, it would be a serious run-time error if there were no protective code to check it. It turns out, however, that the approach of returning a fail or default object works better:

Despite this, the proper approach is to check with isValid() and take appropriate action. To repeat, failure is a normal and significant event, and should be allowed for in code. A common example is when a node stream is read in a loop by nextNode(). When the stream is exhausted, Node::failNode is return to indicate a failure, and that the loop should be exited.

Similarly, a Reader has the interface getChar() to return a Char, and uses a -1 Char as end-of-stream marker, while TokenStream returns an empty DOMString (checked by DOMString::empty(), DOMString::size() or DOMString::getLength()).

Errors

In an ideal world, errors would never be encountered. Limpid attempts to realise the ideal by using protective code. For example, if we invoke NodeList::item(index) and index is either negative or greater than or equal to the count of nodes in the list, Node::failNode is returned; if Node::getParentNode() is invoked and there is no parent, Node::failNode is returned.

In general, if a function would cause a system exception if it is taken to conclusion, it exits. If this is expected to have serious side effects, it throws a system exception.

Although Limpid uses protective code, there is one situation where this is inadequate: in constructors. The most likely difficulty here is when a constructor is unable to allocate memory, with the result that it will have one or more invalid pointers.

It has been pointed out by others that there is very little that we can usefully do in such a situation. Moreover, this difficulty is most likely to be encountered is constructors for the Node, NodeList and NamedNodeMap classes, simply because there are potentially large numbers of them in an application.

For this reason, the approach in Limpid is to catch exceptions only in constructors for the DOM classes: if a std::bad_alloc exception is thrown in setting up one of the pointers of a DOM class, it deletes any valid pointers and rethrows the bad_alloc. The downside of using these exceptions is an increase in execution time of some 10-15%. Exception handling has not been implemented in other class constructors, as it complicates the code and would add further to execution time - and is probably unnecessary because exceptions are unlikely to be thrown in their constructors, simply because there are not likely to be many instances of these classes.

DOM Exceptions

Many of the W3C DOM interface functions specify that a DOMException might be thrown in response to an error. Examples are:

Node::insertBefore(...);
NamedNodeMap::setNamedItem(...)

Limpid conforms to these requirements, but it also allows control of them. An exception is often a drastic over-reaction to an error. Limpid centalises management of errors in a single class, SystemManager. This allows fine-grained control of DOMExceptions, by means of the following interfaces:

void setActive(bool flag);
void setResponse(SystemManager::Response);

The first interface is obvious. In the event that SystemManager is not active, no checks are made, and it is possible, say, to append a Text node to a Document - if you want to. I cannot recommend this option. Setting the response is more useful, and determines the reaction to any of the conditions under which the W3C recommendation requires a DOMException. You can choose throwing a DOMException (THROW), refusing the attemped action and printing an error message (WARN) or silently refusing the action (IGNORE). The default is WARN.

Any sensible application should include exception handling. Just where this is located depends on the application, particularly if you need to recover from an error and take remedial action. Wherever it is placed, you need to consider the three categories:

If, then, a different catch block is used for each category, optimal action can be taken. In the case of a std::bad_alloc exception, for example, you may wish to save the existing document, or at least try to do so.