Exceptions As Constraints

A SelfDocumentingCode practice.

Use errors, assertions and exceptions to document/actualize constraints.

Compare

See Rationale:

Typically, most functions have a restricted domain of input values and only work within a certain subset of the possible states of the system. In any other configuration, the function is undefined, thus forming an error condition. The common methods of dealing with this conditions is through error codes, assertions and exceptions.

As these are the actual filters into the function, they serve to document the domain of the function. They also help reduce bugs (their primary purpose).

Arguments:

"It's difficult to come up with every scenario to verify."

You should be able to do a good job anyway because your functions should not be so complex as to have complex constraints. In most cases, the constraints are such things as checking for NULL (zero) pointers, and number values outside a given range. In the case where there is external (global or class-level) state that needs to be just so for the function to be valid, maybe you should consider decoupling the system more before it turns into spaghetti.

"How is this documentation? People can't see my implementation!"

While the concept that implementation is hidden from client code is generally a good one when designing modular code--leading to decoupled code, it is typically an incorrect assumption. Most code in a system is available for reading.

Also, you are documenting your expectations for the system at a given place in the code, which are very hard things to document outside the code. Just look at the example below regarding the sentinel index. How would you document that in a three-ring binder? "At loop six in function foo() the index must be within the array." What? This becomes very useful when maintaining the code base.

"But this is too complicated. People shouldn't be looking at the implementation when they just want to invoke a method or call a function."

Agreed; you'll still want good function/method comments in the interface declaration. But internally, the constraints are blatantly visible and thus easy to document with comments. Besides, since the constraints are runtime errors, during the development/debug cycles, you can catch misuse of interfaces (albeit, this isn't guaranteed). For example, in Smalltalk, the RedScreenOfDeath is a very good indication of what the system expects from you.

[It's better to catch them during compile time, though.]

"Exceptions are for exceptional circumstances."

Now, looking back on what I've written here, I have come to the conclusion this page is really badly named. I should've named it AssertionsAsConstraints? and possibly ignored exceptions entirely. Exceptions are really, really bad for constraints internal to the system. Why isn't the system in a coherent state?

The only time exceptions should be used as constraints is when an external event invalidates a constraint. Even then, exceptions aren't necessarily the best way to go.

I'll consider moving this page in the future. --ss

Exceptions:

No pun intended.

Debug assertions are kind of an implicit exception because they disappear in release code, thus letting anything through. This can be quite dangerous, but theoretically you have solved all bugs that cause the constraints to be violated.

Aside from that, if anything in the system has a constraint, it is a good idea to check the constraint. It makes for robust code.

CountInAssertions is also an "exception" because it isn't really a constraint. It's a debugging tool.

Examples:

In the following case, the method CString::SetLength() is declared as:

 // Truncates the string to a given length.
 // The length must be <= the current length.
 virtual void SetLength( int iLength );

and defined as:

 void CString::SetLength( int iLength )
 {
assert( !m_iLockCount );
assert( iLength >= 0 && iLength <= m_iStringLength );

// Insert NUL to truncate the string m_szBuffer[iLength] = '\0'; m_iStringLength = iLength; }

Note how the length constraint has been documented for the client. There is an implicit constraint that when a string is locked (and m_iLockCount is non-zero), the string should not be modified. This is an internal constraint to the class that is documented simply through the assert().

Also note how the constraints exist at the top of the method. Grouping them in a highly visible place makes them more readily apparent.

There are several different strengths of constraints. A very strong constraint is a language-level constraint that is enforced by the grammar. For instance, one cannot pass in a const char * to SetLength() as the argument must be promotable to type int.

Exceptions are another strong constraint mechanism as they prevent execution of the code and make a very loud statement that the constraint has been violated. For instance,

 void CFile::Close()
 {
if( !m_bIsOpen )
  throw CFileException( "Cannot close an unopened file." );
 }

ensures that the client code knows that you can only close an opened file. This would be equivalent to

 isOpen ifFalse: [self error: 'Cannot close an unopened file.'.]

in Smalltalk.

Another good method is the use of debug assertions as used above. They make a very loud noise during the development cycle, but quietly go away during a release. However, when they go away, they really go away, thus allowing the system to encounter a destabilizing state unchecked. Exceptions don't suffer this problem.

A weak but effective method is just to return an error code. Typically, you would return true on success and false on error. This reads very well, as in:

 if( !file.Open() )
 {
// Error; could not open the file.
...
 }

The comment is likely useless.

Another good place to verify constraints is in the middle of a code block. For instance,

 CBuffer *pBuffer = GetBuffer();
 assert( pBuffer );

would verify that pBuffer is (likely) a valid object before you use it.

Also,

 // Look for the sentinel node
 int iSentinelIndex;
 for( iSentinelIndex = 0; iSentinelIndex < array.length; iSentinelIndex++ )
if( array[iSentinelIndex] == array.SENTINEL )
  break;
 assert( iSentinelIndex != array.length );

would assert that the array had a sentinel node.

I submit that naming iSentinelIndex just plainly "i" may have improved code readability in this case.

Horror Stories from the Trenches:

[On one project, our interface to the database was through a COM (dynamically linked) object. (Almost) All COM object methods return something called an HRESULT, which is really just an unsigned 32-bit value with a special bit format. This interface checked constraints and constantly returned the very generic E_FAIL error code when an error occurred, which was all and good, but didn't help solve what was wrong with the constraints.

It is fine to simply return a success or fail error code on simple operations, as they have simple error conditions, but complex operations (such as those performed by the database interface) have complex errors. The error code was useless.

In the end, database errors required labourious stepping through code deep into the COM object just to discover the underlying CDBException returned by MFC, complete with a descriptive string detailing the error case.

The moral of the story is, you have to make sure the so-called self-documenting code really does document the code.

-- SunirShah]


CategoryException CategoryAssertions


EditText of this page (last edited April 29, 2006) or FindPage with title or text search