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 errno from the last syscall

Set on syscall failure; meaningless otherwise

$@

The exception caught by the last eval

Set on eval exit (empty string on success)

$?

Exit status of the last child process

Set by every wait/pipe-close/system

$^E

OS-extended error info

Same as $! on Unix; richer on VMS/Win32

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 eval block 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 $@.

  • system, waitpid, wait — populate $?.

  • try/catch — the modern alternative to eval/$@ that side-steps the clobbering hazard.

  • Errno — the source of %! keys; provides named errno constants.

  • Try::Tiny — robust eval wrapper that preserves $@ correctly.

  • %SIG$SIG{__DIE__} and $SIG{__WARN__} are the hooks that fire when die and warn run; they can rewrite $@.

  • Logical operators — the or die idiom that drives almost all $! reporting.