5. Errors

5.1 Introduction

While developing a program, errors are unacceptable mistakes done by programmer often referred to as bugs. To maintain the quality of a program, errors need be removed and a program should be free of errors to be considered acceptable.


The errors can be classified as follows:

Type of Error Found By
Compile-time errors
(Syntax, Type Errors)
Compiler
Link-time errors Linker (During combination of object files into an exceutable program)
Run-time errors Checks (Computer/ Library/ User Code)
Logic errors Programmer (Looking for the causes of erroneous results)

How to deal with errors to produce an acceptable software ?

Answer: Follow these 3 basic approaches

  • Organize software to minimize errors
  • Eliminate most of the errors we made through debugging and testing
  • Make sure the remaining errors are not serious
5.2 Sources of errors
  • Poor specification & Incomplete programs: Occurs when all cases are not handled including error cases.
  • Unexpected arguments: Occurs when a argument is not handled in a function.
    Example: sqrt(-1.2)
    Since sqrt() of a double returns double, there is no possible correct return value.
  • Unexpected input: Occurs when input cases are not handled.
    Example: When user enters a string input instead of a expected integer input
  • Unexpected state: Most programs keep a lot of data around for use by different parts of the system. What if such a data is wrong or incomplete?
  • Logical errors: The code just doesn't do what you meant it to.
5.3 Compile-time errors

Compiler analyzes to detect syntax errors and type errors.
Many of the errors are simply "silly" errors caused due to incomplete edits of the code or mistyping.

5.3.1 Syntax errors

                        
                          int s1 = area(7; // error: ) missing
                          int s2 = area(7) // error: ; missing
                          Int s3 = area(7); // error: Int is not a type
                          int s4 = area('7); // error: non-terminated character (' missing)
                        
                        

For example, given the error in the declaration of s3 above, a compiler is unlikely to say “You misspelled int; don’t capitalize the i.”
Rather, it’ll say something like
“syntax error: missing ‘;’ before identifier ‘s3’”
“‘s3’ missing storage-class or type identifiers”
“‘Int’ missing storage-class or type identifiers”

Drill

Write the code above in a file and see what errors are produced.

5.3.2 Type errors

After syntax errors, the compiler begins looking for type errors.

                        
                          int x0 = arena(7); // error: undeclared function
                          int x1 = area(7); // error: wrong number of arguments
                          int x2 = area("seven",2); // error: 1st argument has a wrong type
                        
                        

5.3.3 Non-errors

                        
                          int x4 = area(10,–7); // OK: but what is a rectangle with a width of minus 7?
                        
                        

For x4, no error message from the compiler. area() asks for two integers irrespective of whether it is positive or negative. Therefore, area (10,–7) is fine.

5.4 Link-time errors

Translational units - several separately compiled parts in a program.

                    
int area(int length, int width); // calculate area of a rectangle
int main()
{
    int x = area(2,3);
}
                    
                    

The linker will complain that it didn't find a definition area(), unless area() is defined in some other source file and it is linked to the generated code from the source file.
Also, both the return type and the argument types must be same as that of the file and the definition of area().

5.5 Run-time errors

Even after the program is syntactically correct it may exit unexpectedly during execution.
In such case it encounters a runtime error.
We say, a program crashes when it halts due to a runtime error.

Examples:

  • division by zero
  • accessing a non-existing file, dictionary value, object attribute or list element
  • using a non-defined identifier
  • operation on incompatible types

                        
                          int area(int length, int width) // calculate area of a rectangle
                          {
                              return length*width;
                          }
                          int framed_area(int x, int y) // calculate area within frame
                          {
                              return area(x–2,y–2);
                          }
                          int main()
                          {
                              int x = –1;
                              int y = 2;
                              int z = 4;
                              // . .
                              int area1 = area(x,y);
                              int area2 = framed_area(1,z);
                              int area3 = framed_area(y,z);
                              double ratio = double(area1)/area3; // convert to double to get
                              // floating-point division
                          }
                        
                      

The above calls lead to negative values of areas, being assigned to area1 and area2.
Also, look at the calculation of the ratio in the code above. It looks innocent enough.
Did you notice something wrong with it?
Look again: area3 will be 0, so that double(area1)/area3 divides by zero.

The above leads to a hardware-detected error that terminates the program. This problem can be dealt in two ways:

  • The caller of area() deal with bad arguments.
  • area() (the called function) deal with bad arguments.

5.5.1 The caller deals with errors

Protect the call of area(x,y) in main():

                            
                              if (x<=0) error("non-positive x");
                              if (y<=0) error("non-positive y");
                              int area1 = area(x,y);
                            
                          

To complete protecting area() from bad arguments, we have to deal with the calls through framed_area():

                            
          if (z<=2)
            error("non-positive 2nd area() argument called by framed_area()");
          int area2 = framed_area(1,z);
          if (y<=2 || z<=2)
            error("non-positive area() argument called by framed_area()");
          int area3 = framed_area(y,z);
                            
                          

This is messy, but there is also something fundamentally wrong.
What if someone modified framed_area() to use 1 instead of 2?
Someone doing that would have to look at every call of framed_area() and modify the error-checking code correspondingly.
Such code is called “brittle” because it breaks easily.

We could make the code less brittle by giving the value subtracted by framed_area() a name:

                            
            constexpr int frame_width = 2;
            int framed_area(int x, int y) // calculate area within frame
            {
            return area(x–frame_width,y–frame_width);
            }
                            
                          

That name could be used by code calling framed_area():

                            
        if (1–frame_width<=0 || z–frame_width<=0)
            error("non-positive argument for area() called by framed_area()");
        int area2 = framed_area(1,z);
        if (y–frame_width<=0 || z–frame_width<=0)
            error("non-positive argument for area() called by framed_area()");
        int area3 = framed_area(y,z);
                            
                          

Look at that code!
Are you sure it is correct? Do you find it pretty? Is it easy to read? Actually, we find it ugly (and therefore error-prone).
We have more than trebled the size of the code and exposed an implementation detail of framed_area(). There has to be a better way!

Look at the original code:

                            
                              int area2 = framed_area(1,z);
                              int area3 = framed_area(y,z);
                            
                          

It may be wrong, but at least we can see what it is supposed to do. We can keep this code if we put the check inside framed_area().

5.5.2 The callee deals with errors

                            
          int framed_area(int x, int y) // calculate area within frame
          {
            constexpr int frame_width = 2;
            if (x–frame_width<=0 || y–frame_width<=0)
              error("non-positive area() argument called by framed_area()");
            return area(x–frame_width,y–frame_width);
          }
                            
                          

This is rather nice, and we no longer have to write a test for each call of framed_area().
Furthermore, if anything to do with the error handling changes, we only have to modify the code in one place.

                            
          int area(int length, int width) // calculate area of a rectangle
          {
            if (length<=0 || width <=0) error("non-positive area() argument");
            return length*width;
          }
                            
                          

This will catch all errors in calls to area(), so we no longer need to check in framed_area().

5.5.3 Error reporting

Once you have checked a set of arguments and found an error, what should you do?
Sometimes you can return an “error value.”
Example:

        
        // ask user for a yes-or-no answer;
        // return 'b' to indicate a bad answer (i.e., not yes or no)
        char ask_user(string question)
        {
          cout << question << "? (yes or no)\n";
          string answer = " ";
          cin >> answer;
          if (answer =="y" || answer=="yes") return 'y';
          if (answer =="n" || answer=="no") return 'n';
          return 'b'; // ‘b’ for “bad answer”
        }


        // calculate area of a rectangle;
        // return –1 to indicate a bad argument
        int area(int length, int width)
        {
          if (length<=0 || width <=0) return –1;
          return length*width;
        }
        
                          

That way, we can have the called function do the detailed checking, while letting each caller handle the error as desired.

This approach seems like it could work, but it has a couple of problems that make it unusable in many cases:

  • Now both the called function and all callers must test. The caller has only a simple test to do but must still write that test and decide what to do if it fails.
  • A caller can forget to test. That can lead to unpredictable behavior further along in the program.
  • Many functions do not have an “extra” return value that they can use to indicate an error. For example, a function that reads an integer from input (such as cin’s operator >>) can obviously return any int value, so there is no int that it could return to indicate failure.

There is another solution that deals with this problem: using exceptions.

5.6 Exceptions

If a function finds an error which it cannot handle, it does not return normally. It throws an exception indicating what went wrong.

  • try block - lists all kinds of exceptions code needs to handle in the catch parts.
  • catch block - specifies what to do if called code uses throw.

5.6.1 Bad arguments

                            
                              class Bad_area { };   // a type specifically for reporting errors from area()

                              // calculate area of a rectangle;
                              // throw a Bad_area exception in case of a bad argument
                              int area(int length, int width)
                              {
                                if (length<=0 || width<=0) throw Bad_area{};
                                  return length*width;
                              }
                            
                          

Bad_area{} means “Make an object of type Bad_area with the default value,”
throw Bad_area{} means “Make an object of type Bad_area and throw it.”

5.6.2 Range errors

                            
                              vector<int> v;    // a vector of ints
                              for (int i; cin>>I; )
                                  v.push_back(i);     // get values
                              for (int i = 0; i<=v.size(); ++i)     // print values
                                  cout << "v[" << i <<"] == " << v[i] << '\n';
                            
                          

The termination condition is i<=v.size() rather than the correct i<v.size().
It is an example of an off-by-one error.

5.6.3 Bad input

Consider reading a floating-point number:

                            
                              double d = 0;
                              cin >> d;
                            
                          

Testing if the last input operation succeeded by testing cin

                            
                              if (cin) {
                                // all is well, and we can try reading again
                              }
                              else {
                                // the last read didn’t succeed, so we take some other action
                              }
                            
                          

One possible reason for operation failure is that there wasn’t a double for >> to read.

                          
            double some_function()
            {
                double d = 0;
                cin >> d;
                if (!cin) error("couldn't read a double in 'some_function()'");
                // do something useful
            }
                          
                          

The condition !cin means that the previous operation on cin failed.

5.6.4 Narrowing errors

Narrowing errors are errors when we assign a value that’s “too large to fit” to a variable, it is implicitly truncated.

                            
                              int x = 2.9;
                              char c = 1066;
                            
                          

Since ints don't have fractional values of an integer, x will get the value 2 rather than 2.9.
And c will get the value 42 (representing the character *) as per ASCII character set.

5.7 Logic errors

After removing initial compiler and linker errors, logic errors occur when the program runs but produces wrong output.

                        
                          int main()
                          {
                              vector<double> temps; // temperatures

                              for (double temp; cin>>temp; ) // read and put into temps
                                  temps.push_back(temp);

                              double sum = 0;
                              double high_temp = 0;
                              double low_temp = 0;

                              for (int x : temps)
                              {
                                  if(x > high_temp) high_temp = x; // find high
                                  if(x < low_temp) low_temp = x; // find low
                                  sum += x; // compute sum
                              }

                              cout << "High temperature: " << high_temp << '\n';
                              cout << "Low temperature: " << low_temp << '\n';
                              cout << "Average temperature: " << sum/temps.size() << '\n';
                          }
                        
                      

For values,
76.5, 73.5, 71.0, 73.6, 70.1, 73.5, 77.6, 85.3, 88.5, 91.7, 95.9, 99.2, 98.2, 100.6, 106.3, 112.4, 110.2, 103.6, 94.9, 91.7, 88.4, 85.2, 85.4, 87.7

The output produced was,
High temperature: 112.4
Low temperature: 0.0
Average temperature: 89.2

Since low_temp was initialized at 0.0, it would remain 0.0 unless one of the temperatures in the data was below zero.

5.8 Estimation

Estimation is a noble art that combines common sense and some very simple arithmetic applied to a few facts. Also sometimes humorously called as guesstimation.

Always ask yourself these questions:

  • Is this answer to this particular problem plausible?
  • How would I recognize a plausible result?

Here, we are not asking, “What’s the exact answer?” or “What’s the correct answer?”
That’s what we are writing the program to tell us.

5.9 Debugging

When you have written some code, you have to find and remove the errors. That process is usually called debugging and the errors are called bugs.

Debugging works roughly like this:

  • Get the program to compile.
  • Get the program to link.
  • Get the program to do what it is supposed to do.


Debugging the most tedious and time-wasting aspect of programming and will go to great lengths during design and programming to minimize the amount of time spent hunting for bugs.

5.9.1 Practical debug advice

Make the program easy to read so that you have a chance of spotting the bugs:

  • Comment your code well
  • The name of the program
  • The purpose of the program
  • Who wrote this code and when
  • Version numbers
  • What complicated code fragments are supposed to do
  • What the general design ideas are
  • How the source code is organized
  • What assumptions are made about inputs
  • What parts of the code are still missing and what cases are still not handled
  • Use meaningful names
  • Use a consistent layout of code
  • Break code into small functions, each expressing a logical action
  • Avoid complicated code sequences
  • Try to avoid nested loops, nested if-statements, complicated conditions
  • Use library facilities rather than your own code when you can
  • A library is likely to be better thought out and better tested than what you could produce as an alternative while busily solving your main problem

5.10 Pre- and post-conditions

Pre-condition Example:

                        
                          int my_complicated_function(int a, int b, int c)
                          // the arguments are positive and a < b < c
                          {
                              if (!(0<a && a<b && b<c)) // ! means “not” and && means “and”
                                  error("bad arguments for mcf");
                              // . . .
                          }
                        
                      

                      
                        int x = my_complicated_function(1, 2, "horsefeathers");
                      
                      

Here, the compiler will catch that the requirement (“pre-condition”) that the third argument be an integer was violated.
Basically, what we are talking about here is what to do with the requirements/pre-conditions that the compiler can’t check.

5.10.1 Post-conditions

                            
                              // calculate area of a rectangle;
                              // throw a Bad_area exception in case of a bad argument
                              int area(int length, int width)
                              {
                                  if (length<=0 || width <=0) throw Bad_area();
                                      return length*width;
                              }
                            
                          

It checks its pre-condition, but it doesn’t state it in the comment (that may be OK for such a short function) and it assumes that the computation is correct (that’s probably OK for such a trivial computation).

However, we could be a bit more explicit:

                            
                              int area(int length, int width)
                              // calculate area of a rectangle;
                              // pre-conditions: length and width are positive
                              // post-condition: returns a positive value that is the area
                              {
                                  if (length<=0 || width <=0) error("area() pre-condition");
                                      int a = length*width;
                                  if (a<=0) error("area() post-condition");
                                      return a;
                              }
                            
                          

We couldn’t check the complete post-condition, but we checked the part that said that it should be positive.

5.11 Testing

Testing is a systematic way to search for errors. “The last bug” is a programmers’ joke. There is no “the last bug” in a large program.

Testing includes comparing the results to what is expected by executing a program with a large and systematically selected set of inputs.

Test Yourself!
  1. The four major types of errors are...
    1. header errors, variable errors, function errors and class errors
    2. compile-time, link-time, run-time, and logical
    3. object errors, inheritance errors, template errors and polymorphism errors
  2. A linker error would occur when...
    1. A function used in your source code can't be found in any linked file or library.
    2. A variable should be linked by assignment to another variable but it is not.
    3. A link between one class and another is missing.
  3. An example of a run-time error is when...
    1. you send the wrong type of argument to a function.
    2. you meant to add tax to the sales price but you subtracted it instead.
    3. your program tries to divide by zero.
    4. you forget the semi-colon at the end of a line of code.
  4. An example of a logic error is when...y
    1. you send the wrong type of argument to a function.
    2. you meant to add tax to the sales price but you subtracted it instead.
    3. your program tries to divide by zero.
    4. you forget the semi-colon at the end of a line of code.
  5. One reason why throwing an exception is better than returning an error value is...
    1. the exception cannot be ignored.
    2. throwing an exception makes the code that produced the error also handle it.
    3. throwing exceptions is a more modern style.
  6. An example of something student programs are not expected to handle is:
    1. bad input
    2. hardware failures
    3. divide-by-zero errors
  7. Most large programs...
    1. will always contain some bugs.
    2. can be fully de-bugged in a couple of days.
    3. will likely be bug-free from the start.
  8. Which is used to handle the exceptions in c++?
    1. exception handler
    2. catch handler
    3. none of the mentioned
    4. handler
  9. Which of the following does not cause a syntax error to be reported by the C++ compiler?
    1. Extra blank lines
    2. Missing ; at the end of a statement
    3. Missing */ in a comment
    4. Mismatched {}
  10. Which of the following is not a syntax error?
    1. std::cout << "Hello world! ";
    2. std::cout << 'Hello world! ';
    3. std::cout << "Hello world! ";
  11. Run Time Errors are ..
    1. the errors which are traced by the compiler during compilation, due to wrong grammar for the language used in the program
    2. the errors encountered during execution of the program, due to unexpected input or output
    3. the errors encountered when the program does not give the desired output
  12. What will happen when the exception is not caught in the program?
    1. error
    2. program will execute
    3. none of the mentioned
    4. block of that code will not execute
Answers

1. b; 2. a; 3. c; 4. b; 5. a; 6. b; 7. a; 8. a; 9. a; 10. c; 11. b; 12. a;

Drill