© Karl Beecher 2018
Karl BeecherBad Programming Practices 101https://doi.org/10.1007/978-1-4842-3411-2_7

7. Error Handling

Karl Beecher1 
(1)
Berlin, Germany
 

Objectives

In this chapter, you’ll learn:
  • Typical error-handling techniques and how to ignore them

  • How to suppress errors

  • How to dodge responsibility for handling errors altogether

  • How to make error-handling as messy an affair as possible

Prerequisites

Before reading this chapter, it will help if you’re familiar with:
  • Assertions

  • Exceptions, including some of the most common exception types in Java (e.g., NullPointerException, IOException)

  • Stack traces

Introduction

Any man can make mistakes, but only a fool persists in his error.

—Cicero

As if you couldn’t guess, making a mess of error-handling is a great way to cause problems in a program. This chapter will discuss various ways of giving bugs the space they need to flourish.

Assume Everything Will Always Go Well

Common advice from programming elders is to assume the worst when writing code. “Things always threaten to go wrong,” the “wise” ones will say, “so program in a way that anticipates errors at any moment.” Poor devils. They may be more experienced, but they’ve allowed their experience to turn them into paranoiacs who live constantly in fear of bugs.

So much can go wrong during the execution of a program, the only teacher who has sensible advice is the ostrich: when trouble brews, just stick your head in the sand and ignore it. It’s the key to a happy life, if not to stable software.

Don’t Check

Chapter 6 already talked about being cautious1 and how such behavior is for losers. Checking inputs before you process them might seem innocuous, but it’s actually the gateway to paranoia. Don’t do it. Otherwise, before you know it, you’ll be writing documentation, adhering to standards, and using bug databases (ugh!). Once that happens, no hope remains for you.

An example of defensive programming is verifying that an input has an expected value before attempting to manipulate it, like this:

if (message != null) {
    System.out.println(message.toUpperCase());
}

Obviously, you should avoid this form, but you should also watch out for defensive programming , which has other manifestations. Some programming constructs offer methods for you to deal with unanticipated outcomes. For example, the switch statement often has an optional default clause in many languages. The code in a default block gets executed when the value of the tested expression matches none of the case values.

String drinkOrder = getNextOrder();
// Maps drinks to prices (in cents)
Map<String, Integer> invoice = getCurrentInvoice();
switch (drinkOrder) {
    case "Cappuccino":
        invoice.put(drinkOrder, 399);
        break;
    case "Latte":
        invoice.put(drinkOrder, 449);
        break;
    case "Mocha":
        invoice.put(drinkOrder, 499);
        break;
    default:
        System.out.println("Unknown drink: " +
                drinkOrder);
        break;
}

In this example, the program matches drinks to prices. If the program doesn’t recognize a drink (an unlikely but nevertheless possible unanticipated outcome), it can’t process the drink’s price, and the user needs alerting of that fact.

Thus, the default clause is a kind of catch-all for miscellaneous or unanticipated outcomes. Suffice it to say, the default clause is a way to sneak in paranoid code that can catch potential problems. Using it is another way those defensive coders try to get you.

Don’t Assert

There’s actually no shortage of ways defensive programmers try to get to you. They offer you tools and techniques like they’re candy, imploring you to “try it and see if you like it.”

Just say no. Otherwise, before you know it, you’ll be hooked.

A particularly powerful tool on offer is assertions , which many programming languages have in some form. An assertion is a statement you can put into a program at a specific point that tests whether a certain condition is true or not. If the condition is true, no further action is taken, but if it’s not, the program typically terminates immediately.2 Here’s an example:

void getTemperatureInKelvin() {
    // Gets a reading in Celsius.
    double temperatureC = getReading();
    // Converts to degrees Kelvin
    temperatureK = temperatureC + 273.15;
    assert temperatureK >= 0 : "Invalid temperature!";
}

Since zero degrees Kelvin is absolute zero (and a lower temperature is a physical impossibility), ending up with a negative value for the temperature in degrees Kelvin means something has gone very wrong.

Pushers of assertions will sell them to you using seductive arguments . “Look,” they’ll say, “see how useful they are . . .” Other arguments include:
  • An easy way to verify your assumptions.

  • So quick to write. Just a single line of code.

  • You’re not forced to use them. In fact, assertions are turned off by default. You have to activate them for assertions to have any effect.3

Naturally, the only acceptable way to use assertions (outside of avoiding them entirely) is to misuse them.

One way to misuse them is to apply them as your exclusive means of error-handling. This takes advantage of their simple binary nature. Either everything is hunky-dory (and the program continues) or something is wrong, causing the program to crash in flames, even if the error is only of the slightest severity. Also, since assertions are typically turned off by default, error-checking done by assertions may as well not exist under normal conditions.

Another way to misuse assertions is to execute state-changing operations inside the assert statement. Look at this:

void haveBirthday() {
    // This method increases age by 1.
    assert (age++ > 0) : "Invalid age!";
}

This code increases age by 1, simulating a birthday. The actual functionality, age++ (which is the same as saying age = age + 1), is combined with the assertion. This cleverly saves a line of code, but also makes sure that the program behaves correctly only when assertions are turned on.

Thumbs Down!

The standard use of assertions is to make clear your assumptions and catch any impossible situations. (Less severe types of problems can be dealt with more subtly using exception handling—see next section.) Assertions often take the form of either a precondition (something that must be true before an operation can take place) or a postcondition (which must be true after an operation takes place). The getTemperatureInKelvin subroutine is an example of a postcondition because it verifies that the calculation has produced a valid result.

Assertions are typically turned on only during development and testing. They’re rarely kept active once a program has been released. That’s why the haveBirthday example is particularly problematic: there’s a chance that the code works fine during development, but stops working as expected once the program has gone into production.

Checking an assertion shouldn’t cause a change in state . A better way to write the haveBirthday method would have been like this:

void haveBirthday() {
    age = age + 1;
    // Postcondition: Age must be greater than zero
    // after having a birthday.
    assert (age > 0) : "Invalid age!";
}

This way, haveBirthday functions whether assertions are active or not.

Don’t Catch

This section began by recommending the ostrich strategy. Here’s where that approach can really pay off.

Programming languages typically have features allowing you to specify what to do in case of a problem. Many of today’s popular languages provide such a feature in the form of exception handling. In Java, potentially problematic code is isolated in a try block , and problems that arise are dealt with in the corresponding catch block.

The great thing about exceptions is that catching them is optional. And, as the first anti-rule of programming says, “Something that is not mandatory is not worth doing.” So, by ignoring the danger, you guarantee that any exception raised gets thrown back at the calling code for someone else to worry about. With luck, that exception never gets caught and causes the program to crash.

Thumbs Down!

Ignoring exceptions is simply dangerous.

An exception tells you a piece of code is unable to do the job expected of it. This is information you need to know because it gives you an opportunity to rescue the program from failure. After all, if a program attempts to open a file using a user-provided name, what’s the reasonable thing to do if the file can’t be found? Crash horribly? Or recognize that a problem occurred and ask the user to input the name again?

Our example language, Java, takes things a little further than other languages by distinguishing between checked and unchecked exceptions .4 Every exception in Java is either one or the other:
  • Unchecked exceptions are intended for serious programming errors considered irrevocable (ESA, 2004). These can optionally be ignored.

  • Checked exceptions are intended for problems that, while rare, nevertheless can happen under normal operation (ESA, 2004). They can’t be ignored, and they form part of a method’s signature.

For example, consider this method:

File getConfigFile() throws IOException

An IOException is a checked exception . Therefore, if you call this method you don’t have the option of ignoring the potential exception. You must enclose the calling code in an appropriate try block.

What to do inside a try block is discussed in the following section.

Send Problems Down the Memory Hole

. . . he crumpled up the original message and any notes that he himself had made, and dropped them into the memory hole to be devoured by the flames.

— George Orwell, Nineteen Eighty-Four (1949)

Ignoring potential problems may only get you so far. Eventually, your colleagues may, shall we say, compel you to recognize that problems can occur in programs and that you should take precautions to handle them. What then are your options?

You don’t want to do effective error-handling, obviously, so you should put ineffective error-handling in place, treating exceptions as unworthy of attention, inconvenient facts that—once identified—ought to be ignored, suppressed, and sent down the memory hole.

Disappearing Exceptions

The previous section advised you to ignore exceptions completely. However, finger-wagging colleagues and overzealous programming languages can conspire to prevent you from doing so. In the end, you might have no choice but to include an error-handling block.

Thankfully, there’s more than one way to ignore an exception. If you’re forced to include a try block , then simply subvert the whole structure. Just because you catch something doesn’t mean you have to do anything with it. Why not just silently drop it?

Look at this example. An application allows a user to set custom settings. It stores that configuration in a file. Every time the application loads, it opens the file, reads the contents, and customizes the environment according to the user’s settings.

// Gets the file location of the application's
// configuration information
File configFile = new File(configFileLocation);
try {
    parseConfigFile(configFile);
    // Code for adjusting app to config settings goes
    // here...
}
catch (FileNotFoundException e) {
    // Leave this empty. Do nothing.
}
Of course, things can go wrong; for instance, the configuration file could go missing. In this case, the application would still function, but it would do so without the user’s custom settings. The effect runs two-fold:
  1. 1.

    The user sees their custom settings have gone missing, but for no good reason. Nothing appeared to explain to them what happened.

     
  2. 2.

    By silently dropping the exception, you leave behind no clue to help the programmer determine the problem in case the user complains (as they are apt to do).

     

Reporting Problems Is Doubleplusungood

A simple and unobtrusive way to deal with problems is to report them. But who wants to be the bearer of bad news? Not you.

However, if your hand is forced and you’re compelled to add some kind of error reporting, you can nevertheless report problems without the risk of being helpful.

You might be told to make the program write messages when something goes wrong. So be it, but make sure you do so as invisibly as possible. For example, if your program is a graphical application, report problems using the standard print statement (like System.out.println) because those messages are sent to the console and will probably go unseen.

Failing this, you might be forced to display prominent messages to the user when a problem occurs. In this situation, it’s best to bamboozle the user with inappropriately technical and complicated information. A message with jargon, error codes , and a stack trace is a good candidate (see Figure 7-1).
../images/455320_1_En_7_Chapter/455320_1_En_7_Fig1_HTML.jpg
Figure 7-1

An example of a bad error message

Better the user is confused than informed.

Thumbs Down!

When you report a problem, the location and content of the report depends on the audience.

An error message for the user should take into account the user’s technical aptitude. Unless you have a good reason to assume otherwise, you should imagine the user to be a non-programmer. Stack traces and error codes won’t help them; you should explain in clear, non-technical language what went wrong and what (if anything) can be done about it. For example:

File configFile = new File(configFileLocation);
try {
    parseConfigFile(configFile);
}
catch (FileNotFoundException e) {
    // Give helpful, non-technical information to
    // the user in a dialog window.
    Alert alert = new Alert(AlertType.ERROR);
    alert.setTitle("Configuration problem");
    alert.setHeaderText("Configuration information was lost or corrupted.");
    alert.setContentText("The application will continue to run with default settings. Please contact your system administrator.");
    alert.show();
}
A message like that in Figure 7-2 will pop-up to the user.
../images/455320_1_En_7_Chapter/455320_1_En_7_Fig2_HTML.jpg
Figure 7-2

An example of a more informative error message

Heavily technical information is useful, but only for the program’s author. That information should be stored in the program’s log for later retrieval when the programmer comes to diagnosing the problem. That means writing messages to a file, not printing them to the console, where they go unrecorded and possibly even unseen. Most programming languages provide their own standard logging functions for this.5

Kick the Can Down the Road

Every problem eventually has to be dealt with by somebody. And preferably somebody else. You can make sure of that by adopting a policy of passing problems onto other areas of the program, ones that are the responsibility of other people.

In other words, kick the can down the road, preferably hard and in a way that’s likely to hurt someone.

Using Error Codes

So, all that previous, sensible advice about dealing with exceptions locally if possible goes out the window. When a problem arises, your code is going to reflexively pass the buck. The question then remains: in what manner should you pass it?

If you can, you should choose a method that’s as uninformative as possible so the receiver of the buck learns little or nothing about the problem. You should also choose a method that passes the buck along so quietly that it can easily be missed.

In most languages, error statuses and error codes can be misused to fit these requirements nicely. We met error codes already in Chapter 3,6 which also pointed out that exceptions are generally preferred over error codes. Naturally, that should be enough to persuade you to prefer error codes. If you need more persuasion, consider some of their delightful drawbacks:
  • Returning an error code forces the caller to deal with an error in one specific place: the place from which they called your subroutine.

  • When new error codes are added, this can mean a program requires recompilation and redeployment. For example, error codes in Java are normally kept in an enum, which is used throughout the system. The effects of updating this enum cascade to other classes in the program far and wide.

As limiting as error codes can be, there’s an even more uninformative alternative: the error flag . A subroutine with a Boolean return type (which holds false in the case that something went wrong) is delightfully simple and wonderfully vague.

boolean succeeded = parseXmlFile(myXmlFile);
if (succeeded) {
    // Do normal business
}
else {
    errorPopup("Parse failed. Don't ask why, because I don't know.");
}

What went wrong in this case? Was the file missing? Did we have insufficient access? Was the XML malformed? The caller simply doesn’t know, and so they’re prevented from taking any informed action.

Perhaps the best problem you cause in either case—whether your subroutine returns a code or a flag—is that the return values can be ignored, or even missed altogether (an easy mistake to make).

Thumbs Down!

Many textbooks and standards documents recommend exceptions over error codes (ESA, 2004; Martin, 2009). Some sources even say you shouldn’t use error codes at all, precisely because they can be ignored (Microsoft, 2017).

Whatever approach you choose, make sure you understand the key differences:
  • When the caller ignores a subroutine’s error code , that code simply “disappears.”

  • When the caller ignores an exception , the exception persists, and it propagates back down the call stack until caught. If it’s never caught, the program crashes.

This is part of what makes exceptions more powerful than error codes. Ignoring an exception might allow you to pass the problem along to be processed at a more appropriate level, but ignoring it completely will not make it go away.

Baffle and Bamboozle

If you lose the fight against exceptions, all is not lost. You could still use exceptions, but in a way that neutralizes some of their advantages, specifically their capacity to be informative. This can leave the caller baffled and bamboozled when they try to handle the exception.

Exceptions allow you to attach additional information, like custom messages. But—keeping in mind our anti-rule that “Anything that isn’t mandatory isn’t worth doing”—why bother, especially if it’s not your code handling the problem. For example, an IOException can be raised when having trouble using an I/O device , but I/O devices are notoriously troublesome, and the root cause could be one of a thousand possible problems.

throw new IOException();

Throwing an exception like this when, say, trying to use a network connection tells the caller only that a problem occurred, but imagine being the poor sap who has to figure out how to react. What exactly was the problem? Was the network unavailable? Was it available but refused access? Was the URI not found?

Or how about this:

throw new IllegalArgumentException();

If your method accepts multiple arguments , then the caller can do little more than guess which one was problematic.

When you think about it, you’re probably being too helpful when you use specifically typed exceptions like NullPointerException , IOException, or IllegalArgumentException. Besides which, choosing between all the different types probably soaks up too much of your precious time. Instead, just use the root Exception class for all problems. Quite the time-saver for you.

Thumbs Down!

When an error occurs, the programmer needs to know key information in order to diagnose it. You colleagues count on you to provide it. That comes partly from helpful error messages .

void assignGrade(Student student, int score)
        throws IllegalArgumentException {
    if (score < 0 || score > 100)
    {
        throw new IllegalArgumentException(
            "Score (" + score +
            ") not in acceptable range (0 to 100).";
        );
    }
    // etc...

It also comes from appropriately typed exceptions. For example, when trying to access a resource over a network , it helps to throw a type that fits the situation rather than just plain old Exception .

ServerResponse response = getNetworkResource(url);
if (response.getCode().equals("400")) {
    // Code 400 means URL was invalid.
    // Caller probably needs to stop and
    // inform the user.
    throw new URIException("Tried to access an" +
            " invalid URL: " + url);
}
if (response.getCode().equals("403")) {
    // Code 403 means access denied.
    // Caller might want to ask the user to
    // enter name and password and then try
    // again to connect.
    throw new AuthenticationException("Access to " +
            url + " denied.");
}

That way, the caller has the option to react in different ways to different problems.

Perhaps the most important thing to ask when handling an exception is: should this code throw an exception at all? Most advice will tell you that if an exception can be handled locally, then it should be. Do everything possible to avoid kicking the can down the road.

Make a Mess

Programs live in a world of their own: a sterile, mathematical world where everything is clean and orderly. The real world, however, is messy and disordered. Errors and exceptions result when these two worlds collide.

Which sounds like the perfect excuse for making error-handling a messy, disordered business.

Cleaning Up and How Not to Do It

Resources live in the real world. They include things like memory , files, networks , and databases . They make computers useful, able to do things like communicate, store information, and display things.

Now, it’s bad enough that resources cause complications even under normal conditions. Being finite in nature, resources require careful management: memory space can’t be exceeded, files shouldn’t be written to simultaneously, databases require users to be authenticated.

But things can get really complicated when you account for the fact that things can go wrong and you need to add error-handling into the mix. Files can disappear unexpectedly, databases can refuse access, and networks have a habit of dying just when you need them most. When things go wrong, your program’s careful management of resources can get thrown out of whack.

Everyone on your project should watch resource-handling code carefully, making sure that resources are properly cleaned up, even in the event of problems. Everyone except you, that is. You’ll be taking advantage of the fact that proper resource management is hard, enabling you to slip in a few easily missed bugs here and there.

Let’s take database connections as an example. A database typically runs as a separate program to which your program must connect in order to access data. It can sustain only a limited number of connections, so each connection must be closed after use.

DbConnection connection =
        database.getConnection(username, password);
ResultSet results = connection.runQuery(
        "SELECT * FROM User WHERE id = " + id);
connection.close();

If a connection is accidentally left open after use, it remains unavailable to everyone else. Forgetting to include cleanup code (like the call to the close method) is easy enough, but getting it wrong under normal conditions is fairly straightforward: either the cleanup code is missing or not.

However, bringing error-handling into it only makes it more complicated and allows you to be wrong in all sorts of other ways.

Accessing a database can go wrong in a number of ways. The connection could be lost, the query could be invalid, authorization might fail, etc. Like a conscientious programmer, you add exception-handling code for such cases:

try {
    DbConnection connection =
            database.getConnection(username, password);
    results = connection.runQuery(
            "SELECT * FROM User WHERE id = " + id);
    connection.close();
}
catch (ConnectionException e) {
    // Thrown if a connection fails
}
catch (QueryException e) {
    // Thrown if a query fails
}
// etc...

And in doing so, you add a bug to the code. Why? Because if an exception is thrown before the connection.close() instruction is reached, the connection will remain open.

Thumbs Down!

You must always clean up resources after use. Since things can go wrong before you get a chance to clean up, use some kind of method that takes account of that.

In most exception-supporting languages, the try block includes a finally clause. Code inside this block is run regardless of whether or not an exception occurred.7

try {
    DbConnection connection =
            database.getConnection(username, password);
    // do stuff with the connection...
}
catch (ConnectionException e) {
    // Thrown if a connection fails
}
catch (QueryException e) {
    // Thrown if a query fails
}
finally {
    connection.close();
}

Even better, if your chosen language can automate the cleaning up of resources, then you can use that. This way, you don’t need to remember to include the code yourself. Since version 1.7, Java has included the try-with block for this purpose.

// DbConnection implements the java.io.AutoCloseable
// interface, so this connection will be automatically
// closed after this try-block exits.
try (DbConnection connection =
        database.getConnection(username, password)) {
    results = connection.runQuery(
            "SELECT * FROM User WHERE id = " + id);
}
catch (QueryException e) {
    // Thrown if a query fails
}

A try-with block differs from a try block by accepting a resource in parentheses after the try keyword (in this example, the connection object). This gives responsibility for closing the resource to the block.

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

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