Skip to content
VLSI Mentor

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

Three failure sources each feeding one Perl error variablethe OS refusedopen, mkdir, unlinka die was caughtinside an eval blocka child finishedsystem or backticks$!errno as text$@the die message$?16-bit wait status12
Each error variable is written by one kind of event and by nothing else. An operating-system call that was refused sets $!, a die that eval trapped sets $@, and a finished child process sets $?. Reading one of them after a different kind of failure gives you a leftover value from earlier, not an empty one.
Azvya Education Pvt. Ltd.VLSI Mentor
three-variables.pl
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;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
$!  No such file or directory
$@  my own error
$?  raw 768, >>8 = 3
$?  raw 15, >>8 = 0, &127 = 15

Read 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.

Azvya Education Pvt. Ltd.VLSI Mentor
wrong-variable.pl
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";
};
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
WRONG  -- this script reports:  cannot compute average: No such file or directory
RIGHT  -- the actual problem:   cannot compute average: Illegal division by zero

Look 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 system or 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 3 gave raw 768, and 768 >> 8 is 3. The exit status lives in the high byte, so 3 << 8 is 768.
  • The child that sent itself SIGTERM gave raw 15. Now >> 8 is 0 — which would read as success if you only looked there — and & 127 is 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:

Azvya Education Pvt. Ltd.VLSI Mentor
decode-status.pl
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);
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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.

Azvya Education Pvt. Ltd.VLSI Mentor
shell-or-not.pl
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";
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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 it

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
what-clobbers-errno.pl
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-$$";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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 REPLACED

Nine 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.

Azvya Education Pvt. Ltd.VLSI Mentor
stale-at.pl
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";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
right after the failing eval  : 'the real problem: bad slack value'
after a later successful eval : ''
the error is GONE

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
the-safe-idiom.pl
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";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
captured safely: cannot read /no/such/file/three: No such file or directory

That 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 trapped die, $? from a child process.
  • $! is the C library's description of errno, after a refused open, mkdir, unlink or chdir.
  • $@ holds the string you gave die, once eval has caught it.
  • $? is a packed 16-bit wait status, not the exit code — measured, exit 3 gives 768.
  • The wrong variable is not empty — it is wrong. Measured: a stale $! from an unchecked open made a division by zero report itself as No such file or directory.
  • Decode $? in three branches: -1 could not start, & 127 killed by that signal, otherwise >> 8.
  • A killed child reads as success if you only shift — >> 8 is 0 while & 127 is 15. That is how a timed-out simulation passes a careless check.
  • One-string system may 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 open clobbers $! with errno 25, Inappropriate ioctl for device. Measured; nine other statements, including two that failed, left it alone.
  • A successful eval erases $@ to the empty string, so anything you call between the eval and the test can lose your error.
  • Copy both into a my variable at once, and build the full message on the same statement as the failure.
  • $! in numeric context is the errno — compare against POSIX::ENOENT, not against English.
  • Use local $@; inside a module so your internal eval does 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.