$SIG{DIE} and the $^S Guard
This page explains how to attach a globaldie handler in Perl without flooding your logs with expected errors. You will learn why the handler fires too early and how to use $^S to filter out the noise.
Your handler will start logging things that are not real crashes. It will catch errors you already handled or errors that$SIG{__DIE__} = sub { log_to_file("FATAL: @_"); };
eval blocks are designed to swallow.
The handler fires on every single die call, even when the program survives the failure.
This is the standard $SIG{__DIE__} trap. The $^S variable is the fix.
Part 1: THE PROBLEM
Here is the scenario. You write a straightforward error handler:A few lines later in your code, you run an$SIG{__DIE__} = sub { my $msg = shift; warn "[CRASH LOG] $msg"; # maybe write to a file, send an alert, etc. };
eval block:
You expect themy $result = eval { some_function_that_might_die(); }; if ($@) { # handle the error gracefully print "Caught it, no big deal\n"; }
eval to catch the die and then handle it quietly. Your $SIG{__DIE__} handler fires anyway, and it fires before eval can step in.
Your crash log now contains an entry for an error that was never a crash. Run that code in a loop or in a module that usesWHAT YOU EXPECT: die() -> eval catches it -> you handle it -> done WHAT ACTUALLY HAPPENS: die() -> $SIG{__DIE__} fires -> eval catches it -> you handle it
eval frequently and your log fills up with noise.
Part 2: WHY IT WORKS THIS WAY
The$SIG{__DIE__} handler fires at the exact moment the die call runs. Perl does not pause to check whether an eval block is waiting to catch the failure. It just runs the handler and then continues with normal die behavior.
The Perl documentation confirms this in perldoc perlvar:
> The $SIG{DIE} hook is called even inside eval()ed blocks/strings.
This is not a bug. It is documented behavior. It is also a trap that catches almost every Perl developer on the first try.
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 block.
The middle value is the useful 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), an eval block is wrapping the call and the failure 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 go to one log. Crashes go to another. Eval'd errors are silently ignored by the die handler because$SIG{__WARN__} = sub { log_message('WARN', @_); }; $SIG{__DIE__} = sub { return if $^S; log_message('FATAL', @_); };
$^S tells you the truth.
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. 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 like closing files, removing temp files, or releasing 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