Chapter 13

Handling Errors and Signals


CONTENTS

Most of the examples in this book have been ignoring the fact that errors can and probably will occur. An error can occur because the directory you are trying to use does not exist, the disk is full, or any of a thousand other reasons. Quite often, you won't be able to do anything to recover from an error, and your program should exit. However, exiting after displaying a user-friendly error message is much preferable than waiting until the operating system or Perl's own error handling takes over.

After looking at errors generated by function calls, we'll look at a way to prevent certain normally fatal activities-like dividing by zero-from stopping the execution of your script; this is by using the eval() function.

Then, you'll see what a signal is and how to use the %SIG associative array to create a signal handling function.

Checking for Errors

There is only one way to check for errors in any programming language. You need to test the return values of the functions that you call. Most functions return zero or false when something goes wrong. So when using a critical function like open() or sysread(), checking the return value helps to ensure that your program will work properly.

Perl has two special variables-$? and $!-that help in finding out what happened after an error has occurred. The $? variable holds the status of the last pipe close, back-quote string, or system() function. The $! variable can be used in either a numeric or a string context. In a numeric context it holds the current value of errno. If used in a string context, it holds the error string associated with errno. The variable, errno, is pre-defined variable that can sometimes be used to determine the last error that took place.

Caution
You can't rely on these variables to check the status of pipes, back-quoted strings, or the system() function when executing scripts under the Windows operating system. My recommendation is to capture the output of the back-quoted string and check it directly for error messages. Of course, the command writes its errors to STDERR and then can't trap them, and you're out of luck.

Once you detect an error and you can't correct the problem without outside intervention, you need to communicate the problem to the user. This is usually done with the die() and warn() functions.

Example: Using the errno Variable

When an error occurs, it is common practice for UNIX-based functions and programs to set a variable called errno to reflect which error has occurred. If errno=2, then your script tried to access a directory or file that did not exist. Table 13.1 lists 10 possible values the errno variable can take, but there are hundreds more. If you are interested in seeing all the possible error values, run the program in Listing 13.1.

Table 13.1  Ten Possible Values for errno

Value
Description
1
Operation not permitted
2
No such file or directory
3
No such process
4
Interrupted function call
5
Input/output error
6
No such device or address
7
Arg list too long
8
Exec format error
9
Bad file descriptor
10
No child processes

Loop from 1 to 10,000 using $! as the loop variable.
Evaluate the
$! variable in a string context so that $errText is assigned the error message associated with the value of $!.
Use
chomp() to eliminate possible newlines at the end of an error message. Some of the messages have newlines, and some don't.
Print the error message if the message is not
Unknown Error. Any error value not used by the system defaults to Unknown Error. Using the if statement modifier ensures that only valid error messages are displayed.

Listing 13.1  13LST01.PL-A Program to List All Possible Values for errno

for ($! = 1; $! <= 10000; $!++) {

    $errText = $!;

    chomp($errText);

    printf("%04d: %s\n", $!, $errText) if $! ne "Unknown Error";

}


Under Windows 95, this program prints 787 error messages. Most of them are totally unrelated to Perl.

Example: Using the or Logical Operator

Perl provides a special logical operator that is ideal for testing the return values from functions. You may recall that the or operator will evaluate only the right operand if the left operand is false. Because most functions return false when an error occurs, you can use the or operator to control the display of error messages. For example:


chdir('/user/printer') or print("Can't connect to Printer dir.\n");

This code prints only the error message if the program can't change to the /user/printer directory. Unfortunately, simply telling the user what the problem is, frequently, is not good enough. The program must also exit to avoid compounding the problems. You could use the comma operator to add a second statement to the right operand of the or operator. Adding an exit() statement to the previous line of code looks like this:


chdir('/usr/printer') or print("failure\n"), exit(1);

print("success\n");

I added the extra print statement to prove that the script really exits. If the printer directory does not exist, the second print statement is not executed.

Note
At the shell or DOS, a zero return value means that the program ended successfully. While inside a Perl script, a zero return value frequently means an error has occurred. Be careful when dealing with return values; you should always check your documentation.

Using the comma operator to execute two statements instead of one is awkward and prone to misinterpretation when other programmers look at the script. Fortunately, you can use the die() function to get the same functionality.

Example: Using the die() Function

The die() function is used to quit your script and display a message for the user to read. Its syntax is


die(LIST);

The elements of LIST are printed to STDERR, and then the script will exit, setting the script's return value to $! (errno). If you were running the Perl script from inside a C program or UNIX script, you could then check the return value to see what went wrong.

The simplest way to use the die() function is to place it on the right side of the or operator


chdir('/user/printer') or die();

which displays


Died at test.pl line 2.

if the /user/printer directory does not exist. The message is not too informative, so you should always include a message telling the user what happened. If you don't know what the error might be, you can always display the error text associated with errno. For example:


chdir('/user/printer') or die("$!");

This line of code displays


No such file or directory at test.pl line 2.

This error message is a bit more informative. It's even better if you append the text , stopped to the error message like this:


chdir('/user/printer') or die("$!, stopped");

which displays


No such file or directory, stopped at test.pl line 2.

Appending the extra string makes the error message look a little more professional. If you are really looking for informative error messages, try this:


$code = "chdir('/user/printer')";

eval($code) or die("PROBLEM WITH LINE: $code\n$! , stopped");

which displays the following:


PROBLEM WITH LINE: chdir('/user/printer')

No such file or directory , stopped at test.pl line 3.

The eval() function is discussed in the section, "Example: Using the eval() Function," later in this chapter. Therefore, I won't explain what this code is doing other than to say that the eval() function executes its arguments as semi-isolated Perl code. First, the Perl code in $code is executed and then, if an error arises, the Perl code in $code is displayed as text by the die() function.

If you don't want die() to add the script name and line number to the error, add a newline to the end of the error message. For example:


chdir('/user/printer') or die("$!\n");

displays the following


No such file or directory

Example: Using the warn() Function

The warn() function has the same functionality that die() does except the script is not exited. This function is better suited for nonfatal messages like low memory or disk space conditions. The next example tries to change to the /text directory. If the connect fails, the consequences are not fatal because the files can still be written to the current directory.


chdir('/text') or warn("Using current directory instead of /text, 

Âwarning");

This line of code displays


Using current directory instead of /text, warning at test.pl line 2.

if the /text directory does not exist. As with die(), you can eliminate the script name and line number by ending your error message with a newline. You could also use the $! variable to display the system error message.

Trapping Fatal Errors

There are times when reporting fatal errors and then exiting the script are not appropriate responses to a problem. For example, your script might try to use the alarm() function, which is not supported in some versions of Perl. Normally, using an unsupported function causes your problem to exit, but you can use the eval() function to trap the error and avoid ending the script.

The eval() function accepts an expression and then executes it. Any errors generated by the execution will be isolated and not affect the main program. However, all function definitions and variable modifications do affect the main program.

Example: Using the eval() Function

You can use the eval() function to trap a normally fatal error:


eval { alarm(15) };

warn() if $@;



eval { print("The print function worked.\n"); };

warn() if $@;

This program displays the following:


The Unsupported function alarm function is unimplemented at test.pl line 

 2.

        ...caught at test.pl line 3.

The print function worked.

The $@ special variable holds the error message, if any, returned by the execution of the expression passed to the eval() function. If the expression is evaluated correctly, then $@ is an empty string. You probably remember that an empty string is evaluated as false when used as a conditional expression.

In an earlier section, "Example: Using the die() Function," you saw the following code snippet being used:


$code = "chdir('/user/printer')";

eval($code) or die("PROBLEM WITH LINE: $code\n$! , stopped");

This program shows that eval() will execute a line of code that is inside a variable. You can use this capability in many different ways besides simply trapping fatal errors. The program in Listing 13.2 presents a prompt and executes Perl code as you type it. Another way of looking at this program is that it is an interactive Perl interpreter.

Loop until the user enters exit.
Print the prompt.
Get a line of input from
STDIN and remove the ending linefeed.
Execute the line.
If the executed code set the
$@ error message variable, display the error message as a warning.

Listing 13.2  13LST02.PL-Using Perl Interactively

do {

    print("> ");

    chop($_ = <>);

    eval($_);

    warn() if $@;

} while ($_ ne "exit");


When you run this program, you will see a > prompt. At the prompt, you can type in any Perl code. When you press Enter, the line is executed. You can even define functions you can use later in the interactive session. The program can be stopped by typing exit at the command line.

If you like powerful command-line environments, you can build on this small program to create a personalized system. For example, you might need to perform a backup operation before leaving work. Instead of creating a batch file (under DOS) or a shell file (under UNIX), you can add a new command to the Perl interactive program, as in Listing 13.3.

Loop until the user enters exit.
Print the prompt.
Get a line of input from
STDIN and remove the ending linefeed.
If the inputted line begins with
do#, then a custom command has been entered.
Process the
do#backup custom command.
See if the user needs help.
Otherwise, use the
eval() function to execute the inputted line.
If the executed code set the
$@ error message variable, display the error message as a warning.

Listing 13.3  13LST03.PL-An Interactive Perl Interpreter that Understands Custom Commands

sub backItUp {

    '\backup /user/*';

    'delete /user/*.bak'

}



sub help {

    print("do#backup will perform the nightly backup\n");

    print("help will display this message.\n\n");

}



do {

    print("> ");

    chop($_ = <>);

    if (/^do#/) {

        backItUp)() if /backup/;

    }

    elsif (/^\s*help/) {

        help();

    }

    else {

        eval($_);

        warn() if $@;

    }

} while ($_ ne "exit");


This program invokes the backup program and deletes the backup files if you enter do#backup at the > prompt. Of course, you need to modify this program to perform the customized commands you'd like to have. This technique also enables you to centralize your administrative tasks, which will make them easier to document and maintain.

Tip
If you are running Perl on a DOS or Windows machine, consider replacing your small batch utility programs with one Perl interpreter and some customized commands. This saves on hard disk space if you use a lot of batch files because each file may take up to 4,096 bytes, regardless of its actual size.

What Is a Signal?

Signals are messages sent by the operating system to the process running your Perl script. At any time, a signal that must be answered can be sent to your process. Normally, a default handler is used to take care of a signal. For example, under Windows 95, when you press the Ctrl+C key combination, your process is sent an INT or interrupt signal. The default handler responds by ending the process and displays the following message:


^C at test.pl line 22

Of course, the filename and line number change to match the particulars of whatever script happens to be running when Ctrl+C was pressed. The ^C notation refers to the Ctrl+C key sequence.

Example: How to Handle a Signal

You can cause Perl to ignore the Ctrl+C key sequence by placing the following line of code near the beginning of your program:


$SIG{'INT'} = 'IGNORE';

You can restore the default handler like this:


$SIG{'INT'} = 'DEFAULT';

If you need to ensure that files are closed, error messages are written, or other cleanup chores are completed, you need to create a custom INT handle function. For example:


sub INT_handler {

    # close all files.

    # send error message to log file.

    exit(0);

}



$SIG{'INT'} = 'INT_handler';

If the Ctrl+C key sequence is pressed anytime after the hash assignment is made, the INT_handler function is called instead of the default handler.

Note
In theory, you could remove the exit() call from the signal handler function, and the script should start executing from wherever it left off. However, this feature is not working on several platforms. If you want to test your platform, run the following small program:
sub INT_handler {
    print("Don't Interrupt!\n");
}

$SIG{'INT'} = 'INT_handler';
for ($x = 0; $x < 10; $x++) {
    print("$x\n");
    sleep 1;
}
You should be able to press Ctrl+C while the script is counting without forcing the script to end.

The %SIG associative array holds only entries you have created for your custom signal handler functions. So, unfortunately, you can't find out which signals are supported by looking at the array returned by keys(%SIG).

Tip
If you are running Perl on a UNIX machine, you can run the kill -l command. This command displays a list of possible signals.

I looked directly into the perl.exe file supplied with my Perl distribution to find out that the hip port of Perl for Win32 supports the following signals:

ABRT2-This signal means that another process is trying to abort your process.
BREAK2-This signal indicates that a Ctrl+Break key sequence was pressed under Windows.
TERM2-This signal means that another process is trying to terminate your process.
SEGV2-This signal indicates that a segment violation has taken place.
FPE2-This signal catches floating point exceptions.
ILL2-This signal indicates that an illegal instruction has been attempted.
INT2-This signal indicates that a Ctrl+C key sequence was pressed under Windows.

You can also use the %SIG hash to trap a call to the warn() and die() functions. This comes in handy if you're working with someone else's code and want to keep a log of whenever these functions are called. Rather than finding every place the functions are used, you can define a handler function as in Listing 13.4.

Define a handler for the warn() function. The error message is passed to the handler as the first element of the @_ array.
Define a handler for the
die() function.
Define the
sendToLogfile() utility function.
Start the signal catching by creating two entries in the
%SIG hash.
Invoke the
warn() and die() functions.

Listing 13.4  13LST04.PL-How to Define Signal Handler Functions for the warn() and die() Functions

sub WARN_handler {

    my($signal) = @_;

    sendToLogfile("WARN: $signal");

}



sub DIE_handler {

    my($signal) = @_;

    sendToLogfile("DIE: $signal");

}



sub sendToLogfile {

    my(@array) = @_;

    open(LOGFILE, ">>program.log");

    print LOGFILE (@array);

    close(LOGFILE);

}



$SIG{__WARN__} = 'WARN_handler';

$SIG{__DIE__}  = 'DIE_handler';



chdir('/printer') or warn($!);

chdir('/printer') or die($!);


When this program is done executing, the PROGRAM.LOG file contains these lines:


WARN: No such file or directory at 13lst02.pl line 22.

DIE: No such file or directory at 13lst02.pl line 23.

Summary

Your program's capability to handle error conditions that may arise will determine, to a certain extent, how usable your program is. If a user of your program finds that it stops working with no error messages and, therefore, no way to solve whatever problem has arisen, then your program won't be used for long.

Displaying error messages is also valuable during the programming and debugging stage. If you mistakenly type a directory name, it may take you an hour to look through the script and find the problem. Handling the No such directory error correctly in the first place will tell you what the problem is and which line of the script has the problem.

In this chapter, you saw that checking for errors usually means looking at the return value of the functions that are called. Some functions set the errno variable while others simply return true or false. While the errno variable does have a core set of values that are system independent, it also has system-dependent values. Listing 13.1 showed you how to display the error values applicable to your system.

Next, you read about the or logical operator. This operator evaluates only the right operand if the left is false. Therefore, it is useful when testing for unsuccessful functions that return false upon failure.

The die() and warn() functions are both used to display an error message. In addition, the die() function causes the script to end.

Then, the eval() function was covered. It is used to execute Perl code in a protected environment so that fatal errors will not end the script. Any error messages that do arise will be placed into the $@ special variable. All variable value changes and function definitions affect the main program.

Lastly, the signals were covered. Signals are messages sent to a process by the operating system. There is a wide range of signals, and they differ depending on which operating system you are using. The %SIG associative array is used to set up your own signal handling function.

The next chapter discusses object orientation. You learn the definition of an object, how to create one, and how to derive new objects from existing objects.

Review Questions

Answers to Review Questions are in Appendix A.

  1. Why is it important to check for errors?
  2. How is the die() function different from the warn() function?
  3. What is the meaning of the $! special variable?
  4. What does the eval() function do?
  5. What is a signal?
  6. What will the statement $SIG{'ABRT'} = 'IGNORE' do?
  7. Which signal is used to trap floating point exceptions?

Review Exercises

  1. Write a program that opens a file and uses the die() function if an error occurs.
  2. Write a program that uses the warn() function if an existing file will be overwritten by an open() statement.
  3. List three situations where the warn() function could be used.
  4. List three situations where the die() function could be used.
  5. Modify the interactive Perl interpreter to print a version number when the version custom command is used.
  6. Modify the interactive Perl interpreter to save all commands entered into a log file. Add a timestamp to each log entry.