Journal Articles

CVu Journal Vol 32, #3 - July 2020 + Process Topics
Browse in : All > Journals > CVu > 323 (11)
All > Topics > Process (83)
Any of these categories - All of these categories

Note: when you create a new publication type, the articles module will automatically use the templates user-display-[publicationtype].xt and user-summary-[publicationtype].xt. If those templates do not exist when you try to preview or display a new article, you'll get this warning :-) Please place your own templates in themes/yourtheme/modules/articles . The templates will get the extension .xt there.

Title: Expect the Unexpected (Part 2)

Author: Bob Schmidt

Date: 09 July 2020 17:15:53 +01:00 or Thu, 09 July 2020 17:15:53 +01:00

Summary: Pete Goodliffe continues to deal with the inevitable.

Body: 

In the previous instalment of this mini-series, we looked at the landscape of ‘error conditions’ in our code. We investigated what errors are, what causes them, how we detect, and how we report error situations.

Now, let’s look at the best strategies to handle error conditions in our code, to ensure our application logic recovers well. Or as well as can be expected. This is where we get practical.

Handling errors

Love truth, and pardon error.
~ Voltaire

Errors happen. We’ve seen how to discover them and when to do so. The question now is: What do you do about them? This is the hard part. The answer largely depends on circumstance and the gravity of an error – whether it’s possible to rectify the problem and retry the operation or to carry on regardless. Often there is no such luxury; the error may even herald the beginning of the end. The best you can do is clean up and exit sharply, before anything else goes wrong.

To make this kind of decision, you must be informed. You need to know a few key pieces of information about the error:

Given this depth of information, you can formulate a strategy to handle each error. Forgetting to insert a handler for any potential error will lead to a bug, and it might turn out to be a bug that is hard to exercise and hard to track down – so think about every error condition carefully.

When to deal with errors

When should you handle each error? This can be separate from when it’s detected. There are two schools of thought.

Thomas Jefferson once declared, “Delay is preferable to error.” There is truth there; the actual existence of error handling is far more important than when an error is handled. Nevertheless, choose a compromise that’s close enough to prevent obscure and out-of-context error handling, while being far enough away to not cloud normal code with roundabout paths and error-handling dead ends.

Handle each error in the most appropriate context, as soon as you know enough about it to deal with it correctly.

Possible reactions

You’ve caught an error. You’re poised to handle it. What are you going to do now? Hopefully, whatever is required for correct program operation. While we can’t list every recovery technique under the sun, here are the common reactions to consider.

Crafting error messages

Inevitably, your code will encounter errors that the user must sort out. Human intervention may be the only option; your code can’t insert a floppy disk or switch on the printer by itself. (If it can, you’ll make a fortune!)

If you’re going to whine at the user, there are a few general points to bear in mind:

  • Users don’t think like programmers, so present information the way they’d expect. When displaying the free space on a disk, you might report Disk space: 10K. But if there’s no space left, a zero could be misread as OK – and the user will not be able to fathom why he can’t save a file when the program says everything’s fine.
  • Make sure your messages aren’t too cryptic. You might understand them, but can your computer-illiterate granny? (It doesn’t matter if your granny won’t use this program – someone with a lower intellect almost certainly will.)
  • Don’t present meaningless error codes. No user knows what to do when faced with an Error code 707E. It is, however, valuable to provide such codes as "additional info" – they can be quoted to tech-support or looked up more easily on a web search.
  • Distinguish dire errors from mere warnings. Incorporate this information in the message text (perhaps with an Error: prefix), and emphasize it in message boxes with an accompanying icon.
  • Only ask a question (even a simple one like Continue: Yes/No?) if the user fully understands the ramifications of each choice. Explain it if necessary, and make it clear what the consequence of each answer is.

What you present to the user will be determined by interface constraints and application or OS style guides. If your company has user interface engineers, then it’s their job to make these decisions. Work with them.

Code implications

Show me the code! Let’s spend some time investigating the implications of error handling in our code. As we’ll see, it is not easy to write good error handling that doesn’t twist and warp the underlying program logic.

The first piece of code we’ll look at is a common error-handling structure. Yet it isn’t a particularly intelligent approach for writing error-tolerant code. The aim is to call three functions sequentially – each of which may fail – and perform some intermediate calculations along the way. Spot the problems with Listing 1.

void nastyErrorHandling()
{
  if (operationOne())
  {
    ... do something ...
    if (operationTwo())
    {
      ... do something else ...
      if (operationThree())
      {
        ... do more ...
      }
    }
  }
}
			
Listing 1

Syntactically it’s fine; the code will work. Practically, it’s an unpleasant style to maintain. The more operations you need to perform, the more deeply nested the code gets and the harder it is to read. This kind of error handling quickly leads to a rat’s nest of conditional statements. It doesn’t reflect the actions of the code very well; each intermediate calculation could be considered the same level of importance, yet they are nested at different levels.

Can we avoid these problems? Yes – there are a few alternatives. The first variant (see Listing 2) flattens the nesting. It is semantically equivalent, but it introduces some new complexity, since flow control is now dependent on the value of a new status variable, ok.

void flattenedErrorHandling()
{
  bool ok = operationOne();
  if (ok)
  {
    ... do something ...
    ok = operationTwo();
  }
  if (ok)
  {
    ... do something else ...
    ok = operationThree();
  }
  if (ok)
  {
    ... do more ...
  }
  if (!ok)
  {
    ... clean up after errors ...
  }
}
			
Listing 2

We’ve also added an opportunity to clean up after any errors. Is that sufficient to mop up all failures? Probably not; the necessary clean-up may depend on how far we got through the function before lightening struck. There are two clean-up approaches:

If you’re not overly concerned about writing Single Entry, Single Exit (SESE) functions, the next example removes the reliance on a separate control flow variable. (Although this clearly isn’t SESE, I contend that the previous example isn’t, either. There is only one exit point, at the end, but the contrived control flow is simulating early exit – it might as well have multiple exits. This is a good example of how being bound by a rule like SESE can lead to bad code, unless you think carefully about what you’re doing.) We do lose the clean-up code again, though. Simplicity renders Listing 3 a better description of the actual intent.

void shortCircuitErrorHandling()
{
  if (!operationOne()) return;
  ... do something ...
  if (!operationTwo()) return;
  ... do something else ...
  if (!operationThree()) return;
  ... do more ...
}
			
Listing 3

A combination of this short circuit exit with the requirement for clean-up leads to the approach in Listing 4, especially seen in low-level systems code. Some people advocate it as the only valid use for the maligned goto. I’m still not convinced.

void gotoHell()
{
  if (!operationOne()) goto error;
  ... do something ...
  if (!operationTwo()) goto error;
  ... do something else ...
  if (!operationThree()) goto error;
  ... do more ...
  return;
error:
  ... clean up after errors ...
}
			
Listing 4

You can avoid such monstrous code in C++ using Resource Acquisition Is Initialization (RAII)) techniques like smart pointers [1]. This has the bonus of providing exception safety – when an exception terminates your function prematurely, resources are automatically deallocated. These techniques avoid a lot of the problems we’ve seen above, moving complexity to a separate flow of control.

The same example using exceptions would look like this (in C++, Java, and C#), presuming that all subordinate functions do not return error codes but instead throw exceptions (see Listing 5).

void exceptionalHandling()
{
  try
  {
    operationOne();
    ... do something ...
    operationTwo();
    ... do something else ...
    operationThree();
    ... do more ...
  }
  catch (...)
  {
    ... clean up after errors ...
  }
}
			
Listing 5

This is only a basic exception example, but it shows just how neat exceptions can be. A sound code design might not need the try/catch block at all if it ensures that no resource is leaked and leaves error handling to a higher level. But alas, writing good code in the face of exceptions requires an understanding of principles beyond the scope of this chapter.

Raising hell

We’ve put up with other people’s errors for long enough. It’s time to turn the tables and play the bad guy: Let’s raise some errors. When writing a function, erroneous things will happen that you’ll need to signal to your caller. Make sure you do – don’t silently swallow any failure. Even if you’re sure that the caller won’t know what to do in the face of the problem, it must remain informed. Don’t write code that lies and pretends to be doing something it’s not.

Which reporting mechanism should you use? It’s largely an architectural choice; obey the project conventions and the common language idioms. In languages with the facility, it is common to favour exceptions, but only use them if the rest of the project does. Java and C# really leave you with no choice; exceptions are buried deep in their execution run times. A C++ architecture may choose to forego this facility to achieve portability with platforms that have no exception support or to interface with older C code.

We’ve already seen strategies for propagating errors from subordinate function calls. Our main concern here is reporting fresh problems encountered during execution. How you determine these errors is your own business, but when reporting them, consider the following:

What kind of errors should you be looking out for? This obviously depends on what the function is doing. Here’s a checklist of the general kinds of error checks you should make in each function:

Exceptions are a powerful error reporting mechanism. Used well, they can simplify your code greatly while helping you to write robust software. In the wrong hands, though, they are a deadly weapon.

I once worked on a project where it was routine for programmers to break a while loop or end recursion by throwing an exception, using it as a non-local goto. It’s an interesting idea, and kind of cute when you first see it. But this behaviour is nothing more than an abuse of exceptions: It isn’t what exceptions are idiomatically used for. More than one critical bug was caused by a maintenance programmer not understanding the flow of control through a complex, magically terminated loop.

Follow the idioms of your language, and don’t write cute code for the sake of it.

Managing errors

The common principle uniting the raising and handling of errors is to have a consistent strategy for dealing with failure, wherever it manifests. These are general considerations for managing the occurrence, detection, and handling of program errors:

Conclusion

To err is human; to repent, divine; to persist, devilish.
~ Benjamin Franklin

To err is human (but computers seem quite good at it, too). To handle these errors is divine.

Every line of code you write must be balanced by appropriate and thorough error checking and handling. A program without rigorous error handling will not be stable. One day an obscure error may occur, and the program will fall over as a result.

Handling errors and failure cases is hard work. It bogs programming down in the mundane details of the Real World. However, it’s absolutely essential. As much as 90 percent of the code you write handles exceptional circumstances [2]. That’s a surprising statistic, so write code expecting to put far more effort into the things that can go wrong than the things that will go right.

Questions

References

[1] Stroustrup (1997) Resource Acquisition Is Initialization (RAII)) techniques like smart pointers

[2] Bentley, Jon Louis (1982) Writing Efficient Programs. Prentice Hall Professional, ISBN-10: 013970244X

Pete Goodliffe Pete Goodliffe is a programmer who never stays at the same place in the software food chain. He has a passion for curry and doesn’t wear shoes.

Notes: 

More fields may be available via dynamicdata ..