◈ Netsurfer / perl.gg / $SIG{DIE} and the $^S Guard

$SIG{DIE} and the $^S Guard

2026-03-25

This page explains how to attach a global die 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.
$SIG{__DIE__} = sub { log_to_file("FATAL: @_"); };
Your handler will start logging things that are not real crashes. It will catch errors you already handled or errors that 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:
$SIG{__DIE__} = sub { my $msg = shift; warn "[CRASH LOG] $msg"; # maybe write to a file, send an alert, etc. };
A few lines later in your code, you run an eval block:
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 and then handle it quietly. Your $SIG{__DIE__} handler fires anyway, and it fires before eval can step in.
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. Run that code in a loop or in a module that uses 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.
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)
The middle value is the useful one. When $^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.
$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 go to one log. Crashes go to another. Eval'd errors are silently ignored by the die handler because $^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.
$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. 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 like closing files, removing temp files, or releasing 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

✓ Document: Donehidden-gems / 15.5 KiB▧