Dealing with Error Messages

Steven Zeil

Last modified: Nov 9, 2023
Contents:

Unless you are a much better programmer than I am, you will almost certainly make some mistakes and get some error messages from the compiler.

This is likely to lead to two problems: capturing the messages, and understanding the messages.

1 Capturing Error Messages

When your programs contain mistakes, compiling them in the command shell can result in large numbers of error messages scrolling by faster than you can read them.

There are two basic ways to deal with this flood. You can use redirection and pipes to send the error messages somewhere more convenient, or you can use IDEs (Integrated Development Environments), programs that launch the compiler and try to capture all output from it.

1.1 Capturing Errors in the Shell

We’ve talked before about how many Unix commands are “filters”, working from a single input stream and producing a single output stream. Actually, there are 3 standard streams in most operating systems: standard input, standard output, and standard error. These generally default to the keyboard for standard input and the screen for the other two, unless either the program or the person running the program redirects one or more of these streams to a file or pipes the stream to/from another program.

1.1.1 Pipes and redirection

We introduced pipes and redirection earlier. The complicating factor here is that what you want to pipe or redirect is not the standard output stream, but the standard error stream. So, for example, doing something like

javac -g *.java > compilation.log

or

javac -g *.java | more

won’t work, because these commands are only redirecting the standard output stream. The error messages will continue to blow on by.

The sequence “2>&1” in a command means “force the standard error to go wherever the standard output is going”. So we can do any of the following:

javac -g *.java > compilation.log 2>&1 
javac -g *.java 2>&1 | more

A useful program in this regard is tee, which copies its standard input both into the standard output and into a named file:

javac -g *.java 2>&1 | tee compilation.log
Example 1: Try This: Capturing error messages
  1. Create a directory in which to practice compiling.

    mkdir ~/playing/sieve
    cd ~/playing/sieve
    cp ~cs252/Assignments/javaSieve/* .
    
  2. Use ls and more to explore the files you have obtained.

  3. Compile the code with the command

     javac -g *.java
    

    You should see a lot of error messages flying by, too quikcly for you to read.

    When the command is done, you will be able to see only the last few error messages. That’s unfortunate, because often it’s the earliest messages that are most meaningful.

  4. Compile the code with the command

     javac -g *.java  > errors.log 2>&1 
     more errors.log 
    

    Now you can see that the first error occurs quite early in the code.

  5. Compile the code again with the command

     javac -g *.java  2>&1 | tee errors2.log
     more errors2.log 
    

    This is very similar to the previous command, but there is, perhaps, less surprise about what will be found in the log file.

1.1.2 Capturing a Script

Another way to capture errors at the command lines is via a program called script.

script” causes all output to your screen to be captured in a file. Just say

script log.txt

and all output to your screen will be copied into log.txt until you say

exit

script output can be kind of ugly, because it includes all the control characters that you type or that your programs use to control formatting on the screen, but it’s still useful. (In particular, if you ever want to capture both the output of some program AND the stuff you typed to produce that output (e.g., so you can send it in an e-mail to someone saying “what am I doing wrong here?”), then script is the way to go.)

1.2 Capturing Messages in an IDE

An IDE (Integrated Development Environment) is a program that assists programmers by combining an editor, a mechanism for launching a compiler and capturing error messages, and usually support for debugging as well.

Most IDEs would need to be run in a graphics-mode session, and we will look at some of those later. But there are two programs of note that can function as an IDE even in a text-mode connection: emacs and vim.

1.3 Compiling with emacs

Example 2: Try This:
  1. Use emacs to edit your Sieve.java program:

    cd ~/playing/sieve  
    emacs -nw Sieve.java
    
  2. Now give the emacs command: M-x compile. (Remember that the Meta key in emacs is most commonly typed by using the escape key, so M-x compile is typed as Esc then xcompile.)

    At the bottom of the screen, you will be asked for the compile command. emacs will suggest the command make -k, a suggestion that will make much more sense after we have looked at make files.

    For now, use the backspace key to remove that suggestion, then type in the proper command just as if you were typing it into the shell.

    In this case, delete the suggested make command and replace it with

     javac -g *.java
    

    emacs will invoke the compiler, showing its output in a window. Figure 1 shows a typical emacs session after such a compilation.

  3. In this case, there should be one or more error messages. The emacs next-error command will move you to the source code location of the first error. That command is given as C-x` – that’s the backtick or backwards apostrophe, usually found on the same key as ~, not the one usually found on the same key as the quotation mark.

    Each subsequent use of C-x ` will move you to the next error location in turn, until all the reported error messages have been dealt with.

    Use this command to step through the errors.

  4. In this case, the problem is very simple. The programmer has a bad habit of holding the shift key down when typing semi-colons, resulting in ‘:’ instead of ‘;’.

    Move your cursor back to the first line of the program. Use the emacs next-error command to step through the errors, fixing them as you go.

    Then use

 

2 Understanding the Error Messages

2.1 Cascading

One thing to keep in mind is that errors, especially errors in declarations, can cascade, with one “misunderstanding” by the compiler leading to a whole host of later messages. For example, if you meant to write

double s = 0.0;

but instead wrote

doubl s = 0.0;

you will certainly get an error message for the unknown symbol doubl. However, there’s also the factor that the compiler really doesn’t know what type s is supposed to be. So every time you subsequently use s e.g.,

s = s + 0.5;

or

doubl y = Math.sqrt(s);

the compiler will probably issue further complaints. Sometimes, therefore, it’s best to stop after fixing a few declaration errors and recompile to see how many of the other messages need to be taken seriously.

2.2 Error Messages versus Mistakes

A compiler can only report where it detected a problem. Where you actually committed a mistake may be someplace entirely different.

For example, suppose that you meant to declare, in MyClass.java, a new function like this:

public MyClass {
    ⋮
    public String getIdentifier() {return theIdentifier;}
    ⋮
}

but that you actually typed:

public MyClass {
    ⋮
    public String getIdentifir() {return theIdentifier;}
    ⋮
}

Now, the compiler has no idea that you meant the function name to be different from what you typed. This is a **perfectly legal function declaration in Java.

But, in a different file, say Application.java, you might have the following code:

    MyClass mc = new MyClass();
    ⋮
    String id1 = mc.getIdentifier();
    ⋮
    String id2 = mc.getIdentifier();
    ⋮
    String id3 = mc.getIdentifier();
    ⋮
    String id4 = mc.getIdentifier();

The compiler will flag each of those four calls to “getIdentifier” as an error, because you have not actually declared a function with that name. It will flag those as compilation errors in Application.java.

The mistake is in one line of MyClass.java. But the compiler will issue error messages in four different locations in Application.java because that’s where the compiler detects a problem.

As you work with more advanced code, you may even encounter situations where the compiler flags errors in code that you did not even write, sometimes code that is part of a system-provided library. That does not mean that the flagged code is actually at fault.

Never assume that the location where a compiler reports an error is where the actual mistake is.

Never assume that, just because the error is flagged in code not belonging to you, that the mistake cannot possibly be yours.

This is also true, perhaps even more so, for runtime errors, exceptions, and program crashes. These often take place in system code, not because the system code is faulty, but because your code supplied faulty inputs to a system function, or to a function called a system function, or to a function that called a function that called…

Never assume that the location where a program crashed is where the actual mistake is.

Be realistic. If things were working for you earlier, or if an entire class or department of students have been happily using a system with it’s built-in libraries, then any code that you have just written that has no history of working correctly is by far the more likely source of your problems.

The most likely location for any mistake is in the code that you wrote most recently.

A corollary: Don’t write dozens or even hundreds of lines of code without compiling and testing. If you check for possible problems after every few lines of code, you are less likely to be uncertain about where the actual mistakes might lie.