Java 101: Exceptions to the programming rules, Part 1

Learn how exception handling has evolved from C to Java

Apart from the simplest programs, programs usually fail. Causes range from coding mistakes to bad user input, to operating system flaws. Regardless of the failure's cause, as a software developer, you're charged with designing programs that can either recover (where the program continues to execute after fixing the failure) or properly shut down (where the program makes every effort to save the user's data before terminating). After all, why should users pay for faulty programs that fail to save data when those programs unexpectedly terminate?

This article discusses program failure in the exception context. After defining exception, I show how C, C++, and Java handle exceptions. Knowing how to handle exceptions in C and C++ gives you an appreciation for why exception handling works the way it does in Java and lets you compare/contrast different exception-handling techniques.

What are exceptions?

Situations arise where programs fail. For example, a program tries to open a file that does not exist. Or a program tries to access a nonexistent array element while ordering a name array. In those examples, the program's proper execution flow diverges into an abnormal flow. In the first example, that divergence occurs when the program fails to open the file. In the second example, the divergence occurs when the program tries to access the nonexistent array element. Each divergence from a proper execution flow to an abnormal one is known as an exception.

Note
Additional examples of commonly occurring exceptions include attempts to write to a printer that is off, read from an unopened file, divide an integer by integer value zero, and call a method using an object reference variable containing a null reference. The first two examples illustrate resource-related exceptions because they involve resources (printer and file). The latter two examples illustrate flawed code exceptions because they arise from improperly written code.

When an exception occurs, the program must handle that exception. Failure to do so puts the program into an unstable state. Under older operating systems, like Microsoft DOS, a program might crash the operating system and reboot the computer. Under newer operating systems, like Linux and Windows XP, the operating system might forcefully terminate the program. Or, instead of terminating, the program might behave erratically. How should a program handle exceptions? The next three sections provide an overview of exception-handling features that C, C++, and Java offer to programs.

Handling exceptions in C

C and many other languages have no technique for detecting -- much less handling -- exceptions arising from flawed code. For those languages, you must carefully write code so such exceptions never occur. In contrast, a technique has always been available for detecting and handling resource-related exceptions -- error code testing.

C developers build programs from function libraries. Think of a function as a method absent from any class. Resource-related functions (such as C's fopen() -- file open -- function) return values to indicate success or failure. Failure values are known as error codes, and the use of program instructions (such as if statements) to examine return values (to see if they represent error codes) is known as error code testing. For example, C's fopen() function returns a FILE structure's address upon successfully opening a file or a NULL error code if it fails to open the file. A C program that calls fopen() typically uses an if statement to determine if that function failed, as the following C-based code fragment indicates:

FILE *fp = fopen ("myfile.dat", "rt"); // Attempt to open file myfile.dat in the read-only and text modes.
if (fp == NULL) // Detect an exception. If fopen() fails, error code testing reveals an exception.
{
    // Handle the exception.
}

Although error code testing is not difficult, it features three problems:

  • Error codes are easy to ignore: Large programs require developers to introduce error code testing in many places. Forgetting to test a function's return value in one place can affect other functions later in the code, leading to difficult-to-find bugs. For example, if a C developer chooses not to test fopen()'s return value for an error code, subsequent calls to the fread() or the fwrite() functions (to read or write to the file, respectively) could also fail -- if fopen() returns an error code.
  • Error code testing often hides the natural execution flow (in source code): When a code sequence contains many calls to resource-related functions, the code needs to test each function call's return value. While reading through the code sequence, a developer can get stuck in an examination of error code testing and miss the natural execution flow.
  • Error code testing can significantly increase a program's size: Duplication of testing and recovery code in multiple places increases program size.

If you are unfamiliar with the concepts of error codes and error code testing, and the aforementioned problems, examine Listing 1's mfc1.c -- Multiple File Copy, Version 1 -- C source code:

Listing 1. mfc1.c

// ==============================
// mfc1.c
//
// Multiple File Copy #1
//
// Created with: Borland C++ 4.52
// ==============================
#include <stdio.h>
void main (int argc, char *argv [])
{
   FILE *fpSrc, *fpDst;
   int byte, i;
   if (argc < 3)
   {
       printf ("usage: mfc1 srcfile [ srcfile ...] dstfile");
       return;
   }
   fpDst = fopen (argv [argc-1], "wb");
   for (i = 1; i < argc-1; i++)
   {
        fpSrc = fopen (argv [i], "rb");
        if (fpSrc == NULL)
        {
            printf ("Could not open %s\n", argv [i]);
            fclose (fpDst);
            return;
        }
        do
        {
           byte = fgetc (fpSrc);
           if (byte == EOF)
               break;
           if (fputc (byte, fpDst) == EOF)
           {
               printf ("Could not write byte\n");
               fclose (fpDst);
               fclose (fpSrc);
               return;
           }
        }
        while (1);
        fclose (fpSrc);
   }
   fclose (fpDst);
}

The mfc1 program copies the contents of one or more source files to a single destination file. After checking for at least three command-line arguments, mfc1 attempts to create the destination file. mfc1 next enters a loop. For each loop iteration, mfc1 attempts to open a source file and copy its contents to the destination file. If all goes well, mfc1 closes the source file and continues with the next iteration. Once mfc1 finishes copying the last source file, mfc1 exits the loop, closes the destination file, and terminates.

Note
I used Borland C++ 4.52 to create mfc1.c. In fact, I used Borland C++ 4.52 to create all of this article's C/C++ programs. If you lack access to Borland C++ 4.52, you can still compile the source code to those programs with other C++ compilers, although you might need to make minor adjustments to the source code.

mfc1 nicely illustrates the three problems with error code testing:

  • mfc1 does not check to see if fopen() succeeds in creating the destination file. That oversight is bad: if fopen() fails, each call to fputc() -- to put a character to the file -- also fails. Fortunately, you can easily resolve that problem by introducing the following source code after fpDst = fopen (argv [argc-1], "wb");:

    if (fpDst == NULL)
    {
        printf ("Could not open %s for writing\n", argv [argc-1]);
        return;
    }
    
  • Despite the small size of mfc1's source code, the source code illustrates error code testing obscuring the program's natural execution flow. The presence of the if statements -- along with the statements they execute when their expressions evaluate to true -- within the for loop contributes to the obscurity. Unfortunately, we cannot eliminate this problem. The best we can do is minimize the amount of code each if statement executes.
  • mfc1's if statements reveal the third problem -- increased code size due to the duplication of fclose (fpDst); and fclose (fpSrc); function calls.

Let's try to reduce the duplication and deal with the other two problems. After some work, we might end up with Listing 2's mfc2.c source code:

Listing 2. mfc2.c

// ==============================
// mfc2.c
//
// Multiple File Copy #2
//
// Created with: Borland C++ 4.52
// ==============================
#include <stdio.h>
void main (int argc, char *argv [])
{
   FILE *fpSrc, *fpDst;
   int byte, i;
   if (argc < 3)
   {
       printf ("usage: mfc2 srcfile [ srcfile ...] dstfile");
       return;
   }
   fpDst = fopen (argv [argc-1], "wb");
   if (fpDst == NULL)
   {
       printf ("Could not open %s for writing\n", argv [argc-1]);
       return;
   }
   for (i = 1; i < argc-1; i++)
   {
        fpSrc = fopen (argv [i], "rb");
        if (fpSrc == NULL)
        {
            printf ("Could not open %s\n", argv [i]);
            goto exit;
        }
        do
        {
           byte = fgetc (fpSrc);
           if (byte == EOF)
               break;
           if (fputc (byte, fpDst) == EOF)
           {
               printf ("Could not write byte\n");
               goto exit;
           }
        }
        while (1);
        fclose (fpSrc);
   }
exit:
   fclose (fpSrc);
   fclose (fpDst);
}

The mfc2 program fixes the problem of ignoring error codes and also reduces the obscurity and duplication problems. For small programs, like Multiple File Copy, error code testing offers a simple solution for dealing with exceptions. But for larger programs, the three problems that error code testing introduces outweigh the benefits of testing error codes. For those programs, developers need a different technique for handling exceptions. Such a technique exists within C++.

Handling exceptions in C++

The error code testing problems in C led to the development of a new exception-handling technique. That technique originated in the paper "Exception Handling for C++," (revised) written by Andrew Koenig and Bjarne Stroustrup, the father of C++. In April 1990, they presented their paper at the Usenix C++ Conference, and the throw-object/catch-object exception handling technique was born. According to that technique, when a function encounters and cannot cope with an exception, the function creates an object describing an exception. The function then throws that object; hopefully, either the function's direct or indirect caller will catch that object. If caught, the object's contents describe the exception. Code that catches the object can use the object's contents to handle the exception.

C++'s reserved word throw followed by a class name specifies an intention to throw an object. The idea is for C++ to create an object from that class and throw the object to C++'s runtime code. The runtime code searches backwards through the function call stack -- from most recently called function to least recently called function -- for a function that contains a catch clause capable of catching objects of the thrown object's class type. When found, execution passes to code found within the catch clause. For example, throw DivideByZero (); creates an object from DivideByZero and throws that object to the runtime code. That code searches the function call stack (backwards) for a function containing catch (DivideByZero). If found, execution passes to code within that clause.

To put throwing and catching objects into perspective, look at Listing 3's excdemo.cpp (C++) source code:

Listing 3. excdemo.cpp

1 2 3 Page 1
Page 1 of 3