Perl Part I · Module 14 · Errors, Warnings, and Debugging
$! , $@ and $? : Three Different Failures
Perl keeps error information in three separate variables, and they are not interchangeable. One holds the operating system's complaint, one holds a fatal error that eval caught, and one holds the exit status of a program you ran. Using the wrong one is the most common error-handling bug in Perl, and it is hard to spot for a reason this page measures: the wrong variable is rarely empty. It usually holds a real message from something that failed earlier, so your script prints a confident, specific, completely unrelated explanation — and you go looking for a missing file when the actual problem was a division by zero.
Intermediate21 min readPerlPart IDebugging
Part I · Module 14 · Chapter 14.3 · $! , $@ and $? : Three Different Failures
1. Three Sources, Three Variables
Each variable has exactly one source. Nothing else writes to it.
Figure 14.3.1 — Which variable holds which failure
use strict;
use warnings;
# 1. $! -- the operating system refused something
my $opened = open(my $fh, "<", "/no/such/file/anywhere");
printf "%-3s %s\n", '$!', $! unless $opened;
# 2. $@ -- a die that eval trapped
eval { die "my own error\n" };
my $trapped = $@;
$trapped =~ s/\s+\z//;
printf "%-3s %s\n", '$@', $trapped if $trapped;
# 3. $? -- a child process finished
# NOTE: system("exit 3") does not work; exit is a shell builtin.
system($^X, "-e", "exit 3");
printf "%-3s raw %d, >>8 = %d\n", '$?', $?, $? >> 8;
# the same variable, for a child killed by a signal instead
system($^X, "-e", 'kill "TERM", $$; sleep 5');
printf "%-3s raw %d, >>8 = %d, &127 = %d\n", '$?', $?, $? >> 8, $? & 127;$! No such file or directory
$@ my own error
$? raw 768, >>8 = 3
$? raw 15, >>8 = 0, &127 = 15Read that line by line:
$! is the operating system's own complaint, as text. No such file or directory is not a Perl message — it is the C library's description of the error number the kernel returned. You get it after a system call that was refused: open, mkdir, unlink, rename, chdir.
$@ is a fatal error that eval caught. It holds the string you passed to die — here, exactly my own error plus the newline the script strips off. Nothing in the operating system touches it. Chapter 14.4 is entirely about this one.
$? is a child process's exit status, and it is not the number you think. exit 3 gave 768, because $? packs two things into one 16-bit value. Section 3 unpacks it.
2. The Trap: The Wrong Variable Is Not Empty
This is the part that matters, and it is why this chapter is rated Critical.
use strict;
use warnings;
# something earlier in the script failed an OS call
open(my $fh, "<", "/no/such/report/file"); # fails, sets $!
# much later, a completely different thing goes wrong
eval {
my $pins = 0;
my $total = 42;
my $avg = $total / $pins; # dies: division by zero
1;
} or do {
my $real = $@;
$real =~ s/\s+\z//;
$real =~ s/ at .* line \d+\.?//;
print "WRONG -- this script reports: cannot compute average: $!\n";
print "RIGHT -- the actual problem: cannot compute average: $real\n";
};WRONG -- this script reports: cannot compute average: No such file or directory
RIGHT -- the actual problem: cannot compute average: Illegal division by zeroLook at what the wrong version printed. Not an empty string. Not a question mark. A real, specific, plausible operating-system error — left over from an open that failed much earlier and was never checked.
A script that prints cannot compute average: No such file or directory sends you hunting for a missing file. There is no missing file in that code path. The actual failure was arithmetic.
The rule that follows is short:
- After
eval, read$@. Never$!. - After a failed
open/mkdir/unlink, read$!. Never$@. - After
systemor backticks, read$?. Never either of the others.
And the reason the mistake survives code review: $! and $@ are both "the error variable" in most people's heads, and both usually contain something, so the output never looks broken.
3. Decoding $? Properly
$? is one 16-bit number holding two separate facts. This is inherited straight from the C wait call.
- High byte — the exit status.
$? >> 8 - Low 7 bits — the signal that killed it, if any.
$? & 127 - Bit 7 — whether a core was dumped.
$? & 128
That explains both measured results from section 1:
exit 3gave raw 768, and768 >> 8is 3. The exit status lives in the high byte, so3 << 8is 768.- The child that sent itself
SIGTERMgave raw 15. Now>> 8is 0 — which would read as success if you only looked there — and& 127is 15, which is SIGTERM.
That second row is the important one. A killed child looks successful if you only shift. A simulation that ran out of memory and got killed, or one you stopped with a timeout, exits by signal. Checking $? >> 8 alone passes it as clean.
So an honest check has three branches, plus a fourth case where there was no child at all:
use strict;
use warnings;
sub describe {
my ($rc) = @_;
return "could not start the child ($!)" if $rc == -1;
return "killed by signal " . ($rc & 127) . (($rc & 128) ? " (core dumped)" : "")
if $rc & 127;
my $code = $rc >> 8;
return $code == 0 ? "finished normally" : "exited with status $code";
}
my @cases = (
[ "a clean run", [ $^X, "-e", "exit 0" ] ],
[ "a failed run", [ $^X, "-e", "exit 7" ] ],
[ "a killed run", [ $^X, "-e", 'kill "TERM", $$; sleep 5' ] ],
);
for my $c (@cases) {
my ($label, $cmd) = @$c;
system(@$cmd);
printf "%-14s raw %-6d %s\n", $label, $?, describe($?);
}
# and the case where no child process ever existed
my $missing = system("this-program-does-not-exist-12345");
printf "%-14s raw %-6d %s\n", "missing tool", $missing, describe($missing);a clean run raw 0 finished normally
a failed run raw 1792 exited with status 7
a killed run raw 15 killed by signal 15
missing tool raw -1 could not start the child (No such file or directory)Four outcomes, four different readings, and only the first means the work was done.
Note that system returns the same value it puts in $?, which is why the last row can capture it in $missing. And note that a missing program gave -1, not an exit status — because with no child, there is nothing to report a status for, and $! is where the reason went.
The punctuation in the command string changes the answer
That -1 is not the whole story, and the exception is worth measuring because it is genuinely surprising. When you call system with one string, Perl decides between two completely different mechanisms based on what is in the string.
use strict;
use warnings;
my @cases = (
[ "bare name", 'this-program-does-not-exist-12345' ],
[ "plus a redirect", 'this-program-does-not-exist-12345 >/dev/null' ],
[ "plus a semicolon", 'this-program-does-not-exist-12345; true' ],
);
for my $c (@cases) {
my ($label, $cmd) = @$c;
my $rc = system($cmd);
printf "%-18s rc %-6d %s\n", $label, $rc,
$rc == -1 ? "-1 : Perl ran it directly, no shell"
: ">>8 = " . ($rc >> 8) . " : a shell ran it";
}bare name rc -1 -1 : Perl ran it directly, no shell
plus a redirect rc 32512 >>8 = 127 : a shell ran it
plus a semicolon rc 0 >>8 = 0 : a shell ran itThe same missing program, three different verdicts:
Bare name gave -1. The string has no shell metacharacters, so Perl skips the shell and tries to run the program itself. There is no program, so there is no child, so there is no status.
Adding >/dev/null gave 32512, which is 127 << 8. Now the string needs a shell, so Perl starts /bin/sh, and the shell is the child. The shell ran, failed to find the program, printed command not found and exited 127 — the conventional status for exactly that. A real child existed, so you get a real status.
Adding ; true gave 0 — success. This is the one to remember. The shell ran two commands and $? reports only the last one. true succeeds, so a missing program reports a clean run.
If you only need to know whether a command worked, Perl lets you write it plainly:
system($prog, @args) == 0 or die "$prog failed: $?\n";0 means a clean exit, so that one-liner is correct for the common case. Use the three-branch describe above when you need to report what happened, or when a signal death must be told apart from a non-zero exit — as it must in any flow that applies a timeout.
4. Check At The Right Moment
The usual advice is that $! is fragile and must be read immediately. That is true, but the reason is not the one people give, so it is worth measuring rather than repeating.
use strict;
use warnings;
my @probes = (
[ 'nothing at all' => sub { } ],
[ 'print "" to stdout' => sub { print "" } ],
[ 'a regex match' => sub { my $s = "abc"; $s =~ /b/; } ],
[ 'sprintf (pure Perl)' => sub { my $s = sprintf "%d", 42; } ],
[ 'stat on a missing file' => sub { stat("/no/such/thing/here") } ],
[ 'opendir on missing dir' => sub { my $d; opendir($d, "/no/such/dir/here") } ],
[ 'a successful mkdir' => sub { mkdir "scratch-dir-$$" } ],
[ 'a successful eval' => sub { eval { 1 } } ],
[ 'system a real child' => sub { system($^X, "-e", "exit 0") } ],
[ 'a successful open' => sub { my $f; open($f, "<", "/etc/hosts"); } ],
);
for my $p (@probes) {
my ($label, $code) = @$p;
open(my $fh, "<", "/no/such/file/zzz"); # always fails, errno 2
my $before = "$!";
$code->();
my $after = "$!";
printf "%-26s errno %-3d %-26s %s\n", $label, $! + 0, $after,
($after eq $before ? "unchanged" : "REPLACED");
}
rmdir "scratch-dir-$$";nothing at all errno 2 No such file or directory unchanged
print "" to stdout errno 2 No such file or directory unchanged
a regex match errno 2 No such file or directory unchanged
sprintf (pure Perl) errno 2 No such file or directory unchanged
stat on a missing file errno 2 No such file or directory unchanged
opendir on missing dir errno 2 No such file or directory unchanged
a successful mkdir errno 2 No such file or directory unchanged
a successful eval errno 2 No such file or directory unchanged
system a real child errno 2 No such file or directory unchanged
a successful open errno 25 Inappropriate ioctl for device REPLACEDNine of those ten statements left $! alone — including print, including a regex, including system, and including a stat and an opendir that themselves failed on a missing path, because they set the same errno 2 and so are indistinguishable.
The one that destroyed it is the last: a successful open. Not a failing one — a successful one. And look at what it left behind: errno 25, Inappropriate ioctl for device. Perl checks whether a freshly opened handle is a terminal, that check fails harmlessly on a regular file, and the leftover error number stays in $!.
So the real hazard is not that a risky statement might overwrite your error. It is that an ordinary, successful, obviously-harmless statement can overwrite it with something absurd. A script that reports cannot read config: Inappropriate ioctl for device has this bug, and the message is bizarre enough to be unsearchable.
Narrowing it further: it is the open and not the close. Measured, close on its own leaves $! at errno 0, the empty string — which is its own kind of lie, because an empty error message reads as "no error".
The same problem with $@
$@ has a sharper version of this, because eval deliberately resets it.
use strict;
use warnings;
eval { die "the real problem: bad slack value\n" };
my $first = $@;
$first =~ s/\s+\z//;
print "right after the failing eval : '$first'\n";
eval { 1 }; # a DIFFERENT eval, which succeeds
my $second = $@;
print "after a later successful eval : '$second'\n";
print "the error is ", ($second eq "" ? "GONE" : "still there"), "\n";right after the failing eval : 'the real problem: bad slack value'
after a later successful eval : ''
the error is GONEThe error is gone. Not corrupted — erased, because a successful eval sets $@ to the empty string, and that is documented behaviour rather than a bug.
This is why calling anything at all between an eval and the test of $@ is unsafe. You do not need to write the second eval yourself; any function you call may contain one, and most library code does. The test then sees an empty $@ and your script continues as though the operation succeeded.
The idiom that avoids all of it
Both problems have the same single fix: turn the fragile global into a stable my variable on the spot.
use strict;
use warnings;
my $path = "/no/such/file/three";
# read $! on the SAME statement as the failure, and build the whole
# message there -- then it is an ordinary string nothing can corrupt
eval {
open(my $fh, "<", $path) or die "cannot read $path: $!\n";
1;
} or do {
my $err = $@; # copy $@ immediately, before anything else
$err =~ s/\s+\z//;
print "captured safely: $err\n";
};
# and the plain non-eval form, which is the one you will write most often
open(my $cfg, "<", $path) or warn "cannot read $path: $!\n";captured safely: cannot read /no/such/file/three: No such file or directoryThat is the whole discipline, and it explains a convention you have been following since Chapter 13.1 without being told why. open ... or die "...: $!" is written as one statement not for brevity, but because by the end of that statement the error has already been turned into a finished string. Nothing that runs afterwards — not a successful open, not somebody else's eval — can touch it.
Three smaller points worth knowing:
$! is a number as well as a string. In numeric context it gives the raw errno: 2 for a missing file, 13 for permission denied, 25 for the ioctl case above. Comparing numbers is reliable in a way that comparing English messages is not, and POSIX supplies the names, so $! == POSIX::ENOENT is a stable test. Chapter 13.5 measured why this matters — two different mistakes produced the same message text.
All three variables are global. They are not scoped to a block or a subroutine, which is exactly why copying into a my variable is the fix.
local is the polite way to use one inside a module. local $@; saves your caller's value and restores it when your scope ends — Chapter 12.8's dynamic scoping, used for its original purpose. Without it, a library that calls eval internally silently erases an error its caller was about to inspect, which is the bug in the previous section seen from the other side.
5. What To Remember
- Three sources, three variables, no overlap —
$!from the OS,$@from a trappeddie,$?from a child process. $!is the C library's description oferrno, after a refusedopen,mkdir,unlinkorchdir.$@holds the string you gavedie, onceevalhas caught it.$?is a packed 16-bit wait status, not the exit code — measured,exit 3gives 768.- The wrong variable is not empty — it is wrong. Measured: a stale
$!from an uncheckedopenmade a division by zero report itself asNo such file or directory. - Decode
$?in three branches:-1could not start,& 127killed by that signal, otherwise>> 8. - A killed child reads as success if you only shift —
>> 8is 0 while& 127is 15. That is how a timed-out simulation passes a careless check. - One-string
systemmay or may not use a shell. Measured, the same missing program gave -1 bare, 127 with a redirect, and 0 with; true— because a compound command reports only its last stage. - Prefer
system($prog, @args)— no shell, no quoting problems, no lost first stage. - A successful
openclobbers$!with errno 25,Inappropriate ioctl for device. Measured; nine other statements, including two that failed, left it alone. - A successful
evalerases$@to the empty string, so anything you call between theevaland the test can lose your error. - Copy both into a
myvariable at once, and build the full message on the same statement as the failure. $!in numeric context is the errno — compare againstPOSIX::ENOENT, not against English.- Use
local $@;inside a module so your internalevaldoes not wipe your caller's error.
Chapter 14.4 takes eval on its own terms: how to wrap something that might die, why the block form and the string form are different tools, and the two mistakes that make an eval look like it is protecting code when it is not.
Where this fits
Part of the Perl curriculum.
