Error state#
Four variables report errors from four different layers. They are not interchangeable and they are populated by entirely different mechanisms; misusing one for another is the most common cause of ”why doesn’t my error handling work“ bugs in Perl code.
Variable | Source | Lifetime |
|---|---|---|
| C library | Set on syscall failure; meaningless otherwise |
| The exception caught by the last | Set on |
| Exit status of the last child process | Set by every |
| OS-extended error info | Same as |
The single best thing you can do for your error handling is to internalise which variable answers which question:
”Did my open/read/connect/syscall fail?“ → check the return value first, then
$!.”Did my
evalblock catch an exception?“ → check$@.”What was the exit code of the program I just ran?“ → decode
$?.”Am I on Windows or VMS and need OS-specific error text?“ → also look at
$^E.
$! — the system error#
$! is the Perl interface to the C library’s errno. It has a dual nature: read it in numeric context to get the integer errno, in string context to get the corresponding error string:
open(my $fh, '<', '/no/such/file') or do {
print "errno = ", $! + 0, "\n"; # numeric: 2
print "errstr = $!\n"; # string: "No such file or directory"
};
Crucially, $! is only meaningful immediately after a failure. The C errno is not cleared on success, so a later read might see a stale value from any earlier system call. The reliable shape:
if (open my $fh, '<', $path) {
# success — $! is meaningless here
process($fh);
} else {
# ONLY here is $! meaningful
die "open $path: $!";
}
Writing the failure on the same line as the operation that caused it is the safest spelling — it leaves no opportunity for an intervening Perl operation to clobber errno.
%! — symbolic errno checks#
%! is a hash whose keys are errno symbol names like ENOENT, EACCES, EAGAIN. A key tests true exactly when $! currently equals that errno:
unless (open my $fh, '<', $path) {
if ($!{ENOENT}) {
return []; # missing file → empty list
}
if ($!{EACCES}) {
die "permission denied: $path";
}
die "open $path: $!"; # anything else: re-raise
}
This is portable — the numeric values of errno symbols differ across operating systems, but %! keys are always the symbol names. Behind the scenes, %! is provided by the Errno module, which is loaded on demand the first time you use the hash.
Assigning to $!#
Writing to $! sets errno in the C library — useful for faking up an error message that downstream code will see:
$! = 13; # EACCES
print "$!\n"; # "Permission denied"
# Slightly more idiomatic — set errno by symbol:
use Errno qw(:POSIX);
$! = EACCES;
die "synthetic permission failure: $!";
$@ — the exception variable#
Set by eval when the block (or string) it ran threw an exception. It contains either the value passed to die (which is most often a string, but can be any reference) or the parsing/runtime error message from the interpreter.
eval {
die "no port configured\n" unless defined $port;
open(my $sock, '<', "/dev/tcp/$host/$port") or die "connect: $!";
1;
};
if ($@) {
warn "could not open $host:$port — $@";
return;
}
Successful evaluation sets $@ to the empty string ''. The if ($@) check is therefore reliable: empty string is false, any non-empty value is true.
The classic $@ clobbering bug#
There is a hazard here. Anything that runs between the eval exit and your if ($@) check can clobber $@. The most common offender is an object’s DESTROY method — when objects go out of scope at the close of the eval block, their DESTROY runs, and if DESTROY itself does any eval, your exception is gone:
eval {
my $obj = MyClass->new;
$obj->might_die;
1;
};
# If MyClass::DESTROY does an eval, $@ is now empty.
warn "got: $@"; # may print nothing!
The historically-recommended fix is to capture $@ immediately, or to use the Try::Tiny module, which handles this and several related pitfalls:
my $err;
{
local $@; # save and isolate
eval {
risky_op();
1;
} or $err = $@ || 'unknown error';
}
if ($err) {
warn "got: $err";
}
The Perl 5.34+ native try/catch feature is the cleaner modern spelling — it does not use $@ at all and is not affected by the destructor problem:
use feature 'try';
try {
risky_op();
} catch ($err) {
warn "got: $err";
}
PetaPerl supports try/catch natively. New code should prefer it.
eval and $!#
eval does not preserve $!. If your eval block did a syscall that failed and then died, $! is whatever the last operation set it to — which inside a destructor or local $SIG{__DIE__} handler can be anything. If your error message needs $!, capture it inside the eval:
eval {
open(my $fh, '<', $path) or die "open $path: $!\n";
...
};
The \n at the end is conventional — it suppresses the ”at line N“ suffix that Perl appends to die messages, which is right for user-facing errors.
$? — the child error#
After a system, waitpid, wait, backtick command, or pipe close, $? contains the 16-bit wait() status of the child process. It is not simply the exit code:
system($cmd, @args);
my $status = $?;
my $exit_code = $status >> 8; # exit value passed to exit() in the child
my $signal = $status & 0x7F; # signal that killed the child, or 0
my $coredump = $status & 0x80; # set if the child dumped core
if ($status == -1) {
die "system: $!"; # could not even fork/exec
} elsif ($signal) {
die "$cmd died with signal $signal";
} elsif ($exit_code) {
die "$cmd failed (exit $exit_code)";
}
The >> 8 shift is the most-cited Perl gotcha there is. Only the upper byte is the exit code; the lower byte holds the signal-and-coredump flags. Forgetting the shift produces values multiplied by 256.
Modern Perl provides the POSIX module’s WEXITSTATUS, WIFSIGNALED, WTERMSIG, WIFEXITED macros, which are the portable named accessors:
use POSIX ':sys_wait_h';
if (WIFEXITED($?)) { my $code = WEXITSTATUS($?); ... }
if (WIFSIGNALED($?)) { my $sig = WTERMSIG($?); ... }
$? is not preserved across other operations. If you need the value, capture it into a local variable on the next line — otherwise the next backtick or system will overwrite it.
$? in END blocks#
Inside an END block, $? contains the intended exit code. Assigning to it overrides the script’s exit status:
END {
$? = 0 if $? == 255 && $shutdown_was_clean;
}
$^E — the extended OS error#
On Unix, $^E is exactly the same as $!. On Windows, it contains the GetLastError() value, which is sometimes more informative than errno (which Win32 maps a coarse subset onto). On VMS, it is the native VMS status code.
PetaPerl is Linux-only. $^E and $! are interchangeable here; the variable exists for portable code that may also run on non-Unix perls.
Putting them together — a worked example#
sub copy_file {
my ($src, $dst) = @_;
open my $in, '<', $src or die "open $src for reading: $!";
open my $out, '>', $dst or die "open $dst for writing: $!";
eval {
local $/ = \65536; # block reads, scoped to this eval
while (defined(my $chunk = <$in>)) {
print {$out} $chunk or die "write $dst: $!";
}
close $out or die "close $dst: $!";
1;
} or do {
my $err = $@ || 'unknown error';
unlink $dst; # clean up the partial file
close $in; # may set $! itself
die "copy_file($src → $dst): $err";
};
close $in or warn "close $src: $!";
return 1;
}
Three error variables in five paragraphs. $! reports each syscall failure on the line that triggered it. $@ collects any exception that crossed the eval boundary. $? is not in this example because there is no child process — but if the function shelled out to cp, you would decode $? in the same shape as the system page.
See also#
die— populates$@. The usual way to raise an exception from your own code.eval— catches exceptions; sets$@.try/catch— the modern alternative toeval/$@that side-steps the clobbering hazard.Errno— the source of%!keys; provides named errno constants.Try::Tiny— robustevalwrapper that preserves$@correctly.%SIG—$SIG{__DIE__}and$SIG{__WARN__}are the hooks that fire whendieandwarnrun; they can rewrite$@.Logical operators — the
or dieidiom that drives almost all$!reporting.