<!-- category: hidden-gems -->
$SIG{DIE} and the $^S Guard
You set up a global die handler. Sensible move. Log every fatal error to a file before the program crashes. Ship the log to your monitoring system. Never miss a crash again.Then your program starts logging things that are not crashes. Errors you already caught. Errors you expected. Errors inside$SIG{__DIE__} = sub { log_to_file("FATAL: @_"); };
eval blocks
that you handle gracefully two lines later.
Your die handler is firing on EVERY die. Even the ones that never
actually kill anything.
Welcome to the $SIG{__DIE__} trap. And welcome to $^S, the
obscure little variable that saves you from it.
Part 1: THE PROBLEM
Here is the scenario. You have a perfectly reasonable error handler:And somewhere else in your code, you do this:$SIG{__DIE__} = sub { my $msg = shift; warn "[CRASH LOG] $msg"; # maybe write to a file, send an alert, etc. };
You expect the eval to catch the die, you handle it, life goes on. But yourmy $result = eval { some_function_that_might_die(); }; if ($@) { # handle the error gracefully print "Caught it, no big deal\n"; }
$SIG{__DIE__} handler ALSO fires. Before the eval catches
anything.
Your crash log now contains an entry for an error that was never a crash. Do this in a loop, or in code that evals frequently, and your log fills up with garbage.WHAT YOU EXPECT: die() -> eval catches it -> you handle it -> done WHAT ACTUALLY HAPPENS: die() -> $SIG{__DIE__} fires -> eval catches it -> you handle it
Part 2: WHY IT WORKS THIS WAY
The$SIG{__DIE__} handler fires at the point of the die call,
before Perl unwinds the stack to find an enclosing eval. Perl does
not peek ahead to see if someone is going to catch this. It just fires
the handler and then proceeds with normal die behavior.
This is documented in perldoc perlvar:
> The $SIG{DIE} hook is called even inside eval()ed blocks/strings.
It is not a bug. It is documented behavior. But it is a trap that catches almost everyone the first time.
die("oops") | v $SIG{__DIE__} fires <-- HERE, before eval sees it | v Stack unwinds | v eval {} catches it | v $@ is set to "oops"
Part 3: ENTER $^S
The variable$^S (also known as $EXCEPTIONS_BEING_CAUGHT if you
use English) tells you whether Perl is currently inside an eval.
That middle value is the golden one. WhenVALUE MEANING ----- ---------------------------------------- undef Parsing/compilation phase (BEGIN blocks) 0 Not inside an eval (die will kill you) 1 Inside an eval (die will be caught)
$^S is false (0), the die
is a genuine, unhandled crash. When $^S is true (1), someone has an
eval wrapped around this, and the die is expected error handling.
Part 4: THE FIX
One line. That is all it takes.The$SIG{__DIE__} = sub { return if $^S; # inside an eval, not a real crash my $msg = shift; warn "[CRASH LOG] $msg"; };
return if $^S guard skips the handler when you are inside an
eval block. Only genuine, uncaught deaths trigger your logging.
Before and after:
Clean. Precise. Exactly what you wanted.# WITHOUT $^S guard: eval { die "expected\n" }; # handler fires (bad) die "real crash\n"; # handler fires (good) # WITH $^S guard: eval { die "expected\n" }; # handler skipped (good) die "real crash\n"; # handler fires (good)
Part 5: A COMPLETE LOGGING PATTERN
Here is a production-ready die handler with the$^S guard and
proper file logging:
The#!/usr/bin/env perl use strict; use warnings; use POSIX qw(strftime); # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Global crash logger # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ my $log_file = '/var/log/myapp/crashes.log'; $SIG{__DIE__} = sub { # skip if inside eval (expected error handling) return if $^S; my $msg = shift; chomp $msg; my $timestamp = strftime("%Y-%m-%d %H:%M:%S", localtime); my $caller = (caller(0))[1] . ':' . (caller(0))[2]; if (open my $fh, '>>', $log_file) { print $fh "[$timestamp] [$caller] FATAL: $msg\n"; close $fh; } }; # This eval'd die does NOT trigger the handler eval { die "handled error"; }; print "Caught: $@" if $@; # This bare die DOES trigger the handler die "unhandled crash";
caller(0) call gives you the file and line number where the
die happened. Combined with a timestamp, you get a useful crash log
entry like:
[2026-03-25 14:30:00] [script.pl:42] FATAL: unhandled crash
Part 6: THE WARN HANDLER TOO
While we are at it,$SIG{__WARN__} is the same idea for warnings.
It fires on every warn call. No eval issue here since warnings do
not propagate, but the pattern is similar:
You can pair both handlers for a complete logging setup:$SIG{__WARN__} = sub { my $msg = shift; chomp $msg; my $timestamp = strftime("%Y-%m-%d %H:%M:%S", localtime); print STDERR "[$timestamp] WARN: $msg\n"; }; warn "disk space low"; # Output: [2026-03-25 14:30:00] WARN: disk space low
Clean symmetry. Warnings logged, crashes logged, eval'd errors ignored.$SIG{__WARN__} = sub { log_message('WARN', @_); }; $SIG{__DIE__} = sub { return if $^S; log_message('FATAL', @_); };
Part 7: THE UNDEF CASE
Remember the third value of$^S? When it is undef, you are in
the compilation phase. BEGIN blocks, use statements, that sort of
thing.
Most of the time you do not need this level of detail. The simple$SIG{__DIE__} = sub { if (!defined $^S) { # compilation phase - syntax error or BEGIN block death warn "[COMPILE ERROR] @_"; } elsif ($^S) { # inside eval - expected return; } else { # runtime, not in eval - genuine crash warn "[RUNTIME CRASH] @_"; } };
return if $^S covers 99% of cases. But if you are building a
framework or a test harness, knowing the difference between compile
errors and runtime crashes matters.
$^S value | +----+----+ | | | undef 0 1 | | | compile | eval phase | block | genuine crash
Part 8: LOCAL SCOPE HANDLERS
You do not have to set$SIG{__DIE__} globally. You can scope it
with local:
This is great for wrapping a specific subsystem with its own error logging without polluting the global handler. When the block exits, the previous handler comes back automatically.{ local $SIG{__DIE__} = sub { return if $^S; warn "Scoped handler caught: @_\n"; }; # dies in this block use the local handler risky_operation(); } # out here, the previous handler (or none) is restored
You can also combine this with eval for a try/catch pattern that logs unexpected failures:
sub try_with_logging { my ($code) = @_; local $SIG{__DIE__} = sub { return if $^S; warn "[UNEXPECTED] @_"; }; my $result = eval { $code->() }; return ($result, $@); } my ($val, $err) = try_with_logging(sub { do_something_risky(); });
Part 9: COMMON MISTAKES
A few pitfalls to avoid.Dying inside the die handler. If your $SIG{__DIE__} handler
itself calls die, you get infinite recursion. Perl will eventually
give up, but it is ugly. Keep your handler simple. Write to a file,
set a flag, print to STDERR. Do not call anything that might die.
Forgetting that $^S is undef during compilation. The guard# BAD - the open could die, triggering the handler again $SIG{__DIE__} = sub { return if $^S; open my $fh, '>>', $log or die "Can't open log: $!"; # BOOM print $fh "@_"; close $fh; }; # BETTER - fail silently if logging fails $SIG{__DIE__} = sub { return if $^S; if (open my $fh, '>>', $log) { print $fh "@_"; close $fh; } };
return if $^S does NOT trigger when $^S is undef (since undef
is false). So compilation-phase dies will still fire your handler. If
you only want runtime crashes:
Using $SIG{DIE} instead of END blocks. If you need cleanup (close files, remove temp files, release locks), use anreturn unless defined $^S && !$^S;
END block.
Die handlers are for logging and notification, not cleanup.
END { unlink $tempfile if $tempfile && -e $tempfile; }
Part 10: PUTTING IT ALL TOGETHER
Here is the complete pattern. A robust, production-grade error handling setup in about 30 lines:Three concerns, three handlers, zero overlap. Warnings go to one log. Crashes go to another. Eval'd errors are silently ignored by the die handler because#!/usr/bin/env perl use strict; use warnings; use POSIX qw(strftime); my $log_dir = '/var/log/myapp'; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Warning handler - log all warnings # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ $SIG{__WARN__} = sub { my $msg = shift; chomp $msg; my $ts = strftime("%Y-%m-%d %H:%M:%S", localtime); if (open my $fh, '>>', "$log_dir/warnings.log") { print $fh "[$ts] $msg\n"; close $fh; } print STDERR "[$ts] WARN: $msg\n"; }; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Die handler - log only real crashes # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ $SIG{__DIE__} = sub { return if $^S; # the $^S guard my $msg = shift; chomp $msg; my $ts = strftime("%Y-%m-%d %H:%M:%S", localtime); my @caller = caller(0); if (open my $fh, '>>', "$log_dir/crashes.log") { print $fh "[$ts] $caller[1]:$caller[2] FATAL: $msg\n"; close $fh; } }; # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Cleanup on exit # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ END { # release locks, close connections, etc. }
$^S tells you the truth.
Most Perl programmers discover.--. |o_o | "$^S: one variable, |:_/ | zero false alarms." // \ \ (| | ) /'\_ _/`\ \___)=(___/
$SIG{__DIE__} early. Most of them
get burned by the eval problem shortly after. And most of them
either give up on die handlers entirely or live with noisy logs
for years before someone mentions $^S.
Now you know. Three characters. One guard. Problem solved.
perl.gg