2.7.2. Debugging Run-time Errors

run-time error (definition)

Programs that contain run-time errors compile, but they "crash" or fail to end when they run. If a program crashes, the system may display a diagnostic that helps programmers locate the error, but a program that fails to terminate produces no diagnostics. In either case, locating the error is the first step programmers must take to identify and correct it. Run-time errors are typically a sub-class of logical errors, which suggests that programmers locate them by instrumenting the code or using a debugger.

Fortunately, none of the programming operations introduced in the first chapters are likely to cause a run-time error. However, this situation makes it difficult to demonstrate a run-time error by causing one. So, the following examples rely on trivial pseudocode to illustrate the debugging techniques.

Bracketing A Run-time Error

run-time error, tracing, bracketing

Run-time errors are difficult to locate because they run many statements before unexpectedly experiencing a catastrophic failure. Programmers use this same behavior to locate the error. If run-time errors are a sub-class of logical errors, then it seems reasonable to locate them using a similar strategy. Step 2 of the logical-error-location strategy is, "Trace the calculations through program execution." While intermediate values may help programmers correct a run-time error, the primary goal is locating the statement causing it. To locate a run-time error, programmers bracket it with tracing operations implemented with instrumentation statements or a debugger. Initially, the tracing operations may surround many programming statements, but are moved closer together, narrowing the search.

Begin by placing two tracing operations in the program experiencing a run-time error. Place the first operation before the suspected error location and the second after.
  1. If neither operation displays information
    1. The run-time error lies below the second tracing operation
    2. Move the bottom tracing operation downward
  2. If both operations display information
    1. None of the statements between the two operations cause the run-time error
    2. Move both tracing operations downward to surround another block of suspect code
  3. If the first operation displays information but the second does not
    1. If there is only one statement between the tracing operations, or the error is obvious
      1. Begin debugging, treating the problem as a logical or run-time error as appropriate
    2. The run-time error lies between the two tracing operations
      1. Move the two output statements closer together: move the top operation down, the bottom operation up, or both
      2. Repeat the run-time error location strategy
Strategy for locating a run-time error. The goal is to reduce the amount of code that can potentially cause a run-time error. Focusing our attention on a small, restricted region is often enough to help us recognize and correct the error. If we can't identify the error, the narrowed region gives us a smaller search area for debugging.

While the strategy describes bracketing a run-time error with two tracing operations, programmers are not limited to only two. Sometimes, especially at the beginning of the process, it's desirable to use multiple operations spread throughout the suspect code. Regardless of the number of operations, programmers apply the strategy to adjacent pairs.

Locating A Run-time Error: Manually Instrumenting The Code

run-time error, instrument code, Bad Tuna, preprocessor, __FILE__, __LINE__

While the values of variables and other expressions may ultimately help debug a run-time error, locating it is the initial step. To trace program execution, programmers insert statements that signal, "The program made it this far before failing." So, the only requirement of the instrumented code's output is that it uniquely identifies a reached location in the program. I suspect that this was the purpose of the "Bad Tuna" message described previously.

cerr << "Start" << endl;
statement 1;
statement 2;
statement 3;
	...
statement n;
cerr << "Done" << endl;
statement 1;
cerr << "Start" << endl;
statement 2;
cerr << "Done" << endl;
statement 3;
	...
statement n;
cerr << __FILE__ << " - " << __LINE__ << endl;
statement 1;
statement 2;
statement 3;
	...
statement n;
cerr << __FILE__ << " - " << __LINE__ << endl;
(a)(b)(c)
Locating run-time errors with instrumented code. Authentic programs typically consist of many statements, often spread over many files. So, when a program exhibits a run-time error, programmers often instrument it to locate the error. However, run-time errors either cause a program to "crash" (abort abruptly) or never terminate. Either way, the program ceases its normal operation before the instrumentation can announce the error, its cause, or its location. Consequently, programs must execute the instrumentation before the run-time error occurs. The example highlights the instrumentation statements in green and the statement causing the run-time error in red.
  1. Programmers begin by surrounding or bracketing suspect statements with instrumenting code.
  2. They iteratively move the bracketing instrumentation closer together until they identify the statement causing the run-time error.
  3. Syntax-error-diagnostics include the file name and (approximate) line number of the error, and assembly-error-diagnostics include the file name. For large, complex programs, especially those with functions, you may need to use many sets of bracketing operations. The file name and line number of a specific instrumenting statement are beneficial in at least two ways:
    1. Together, the file name and line number uniquely identify each instrumenting statement without requiring an additional label. They also help programmers locate run-time errors when a program contains many sets of bracketing statements.
    2. The file name and line number help programmers find and remove the instrumenting statements when they are no longer needed.
    The preprocessor replaces two predefined variables with the file name and line number, respectively:
    • __FILE__
    • __LINE__

Locating A Run-time Error: Using A Debugger

run-time error, debugger, breakpoint, tracepoint, $FUNCTION, $FILEPOS, Ctrl-Shift-F9, Delete All Breakpoints

The logic and bracketing process used to locate run-time errors is independent of how programmers perform the process, implying they can use either manual instrumentation or a debugger. Nevertheless, a debugger automates and simplifies the task, and has the advantage that removing the tracing operations is fast and easy. Programmers implement tracepoints in Visual Studio as illustrated in Figure 10 of the previous section, or they can leverage tracepoints' status as specialized breakpoints as illustrated in the following figure:

A picture showing a breakpoint circle with a 'Settings' menu displayed when the programmer hovers the mouse pointer over the circle. The menu has a button labeled with a gear icon, which converts the breakpoint to a tracepoint and shows the configuration menu below. Programmers create a breakpoint by left-clicking in the grey column next to a statement, then convert it to a tracepoint by right-clicking the circle or by hovering the mouse pointer over the circle and pressing the "gear" button in the pop-up menu.
A picture showing the tracepoint configuration menu. Checking the 'Action' checkbox displays the Action input field. Checking the "Actions" checkbox converts a breakpoint to a tracepoint. Programmers can enter labels such as "Start" or "Done" or evaluate expressions as illustrated in the previous section. Typing a "$" in the input field displays another menu. Two predefined action variables, $FUNCTION and $FILEPOS, display the file's full name and the statement's line number in the tracepoint output. Tracepoints must be attached to an executable statement, so the example displays line numbers 26 and 32.
A picture showing two bracketing tracepoints. Following the Figure 1 strategy, programmers bracket blocks of code suspected of containing a run-time error with tracepoints, and move the tracepoints together to locate the error. Like breakpoints, programmers remove all tracepoints with the menu "Debug → Delete All Breakpoints" or the keyboard shortcut Ctrl-Shift-F9.
Locating run-time errors with a debugger.