perl.gg / hidden-gems

<!-- category: hidden-gems -->

$SIG{DIE} and the $^S Guard

2026-03-25

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.
$SIG{__DIE__} = sub { log_to_file("FATAL: @_"); };
Then your program starts logging things that are not crashes. Errors you already caught. Errors you expected. Errors inside 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:
$SIG{__DIE__} = sub { my $msg = shift; warn "[CRASH LOG] $msg"; # maybe write to a file, send an alert, etc. };
And somewhere else in your code, you do this:
my $result = eval { some_function_that_might_die(); }; if ($@) { # handle the error gracefully print "Caught it, no big deal\n"; }
You expect the eval to catch the die, you handle it, life goes on. But your $SIG{__DIE__} handler ALSO fires. Before the eval catches anything.
WHAT YOU EXPECT: die() -> eval catches it -> you handle it -> done WHAT ACTUALLY HAPPENS: die() -> $SIG{__DIE__} fires -> eval catches it -> you handle it
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.

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.
VALUE MEANING ----- ---------------------------------------- undef Parsing/compilation phase (BEGIN blocks) 0 Not inside an eval (die will kill you) 1 Inside an eval (die will be caught)
That middle value is the golden one. When $^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.
$SIG{__DIE__} = sub { return if $^S; # inside an eval, not a real crash my $msg = shift; warn "[CRASH LOG] $msg"; };
The return if $^S guard skips the handler when you are inside an eval block. Only genuine, uncaught deaths trigger your logging.

Before and after:

# 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)
Clean. Precise. Exactly what you wanted.

Part 5: A COMPLETE LOGGING PATTERN

Here is a production-ready die handler with the $^S guard and proper file logging:
#!/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";
The 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:
$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
You can pair both handlers for a complete logging setup:
$SIG{__WARN__} = sub { log_message('WARN', @_); }; $SIG{__DIE__} = sub { return if $^S; log_message('FATAL', @_); };
Clean symmetry. Warnings logged, crashes logged, eval'd errors ignored.

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.
$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] @_"; } };
Most of the time you do not need this level of detail. The simple 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:
{ 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
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.

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.

# 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; } };
Forgetting that $^S is undef during compilation. The guard 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:
return unless defined $^S && !$^S;
Using $SIG{DIE} instead of END blocks. If you need cleanup (close files, remove temp files, release locks), use an 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:
#!/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. }
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 $^S tells you the truth.
.--. |o_o | "$^S: one variable, |:_/ | zero false alarms." // \ \ (| | ) /'\_ _/`\ \___)=(___/
Most Perl programmers discover $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