perl.gg / hidden-gems

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

$SIG{WARN} - Intercepting Every Warning

2026-04-19

Perl warnings are useful. Until there are 400 of them scrolling past while you try to find the one that matters.
$SIG{__WARN__} = sub { my $msg = shift; # you now control every warning in the program };
One assignment. Every warn call, every internal Perl warning, every "use of uninitialized value" message now passes through your handler before it goes anywhere.

You can timestamp them. Redirect them to a log file. Filter the noise. Promote the important ones to fatal errors. Count them and summarize at the end. Whatever you need.

Part 1: THE BASIC HOOK

The $SIG{__WARN__} handler intercepts all warnings before they reach STDERR:
$SIG{__WARN__} = sub { my $msg = shift; print STDERR "CAUGHT: $msg"; }; warn "something happened"; # Output: CAUGHT: something happened at script.pl line 7.
The handler receives the full warning message as its first argument, including the "at file line N" suffix that Perl appends.

If your handler does nothing (empty sub), warnings are silently swallowed:

$SIG{__WARN__} = sub { }; # silence everything
This is a terrible idea in production. But sometimes during development, when you are testing one specific thing and 300 deprecation warnings are drowning the output, it buys you five minutes of peace.

Part 2: ADDING TIMESTAMPS

The single most useful thing you can do with a warn handler is add timestamps:
use POSIX qw(strftime); $SIG{__WARN__} = sub { my $msg = shift; chomp $msg; my $ts = strftime("%Y-%m-%d %H:%M:%S", localtime); print STDERR "[$ts] $msg\n"; }; warn "disk space low"; # Output: [2026-04-19 14:30:00] disk space low
The chomp removes Perl's trailing newline, then we add our own formatted line with the timestamp prefix. Every warning in the entire program now gets a timestamp.

For long-running daemons, this is essential. Without timestamps, your log is just a pile of messages with no way to correlate them to events.

Part 3: REDIRECTING TO A LOG FILE

Send warnings to a file instead of STDERR:
my $log_file = '/var/log/myapp/warnings.log'; $SIG{__WARN__} = sub { my $msg = shift; chomp $msg; if (open my $fh, '>>', $log_file) { my $ts = strftime("%Y-%m-%d %H:%M:%S", localtime); print $fh "[$ts] $msg\n"; close $fh; } else { # fallback to STDERR if we can't open the log print STDERR $msg, "\n"; } };
Notice the fallback. If the log file cannot be opened (permissions, disk full, directory missing), we fall back to STDERR instead of silently losing the warning. Never lose warnings. That is the whole point of having them.

The open-write-close pattern on every warning is intentional. It keeps the filehandle from staying open between warnings, which matters if your log rotator needs to move the file.

Part 4: FILTERING SPECIFIC WARNINGS

Some warnings are noise. You know about them. You have decided they are acceptable. Filter them out:
$SIG{__WARN__} = sub { my $msg = shift; # suppress specific known warnings return if $msg =~ m~Subroutine .+ redefined~; return if $msg =~ m~Use of uninitialized value in concatenation~; return if $msg =~ m~Wide character in print~; # let everything else through warn $msg; # NO! infinite loop! };
Wait. That last line calls warn inside a $SIG{__WARN__} handler. That triggers the handler again. Infinite loop. Perl actually detects this and prints the warning directly to STDERR on the second call, but it is still sloppy.

The correct way:

$SIG{__WARN__} = sub { my $msg = shift; return if $msg =~ m~Subroutine .+ redefined~; return if $msg =~ m~Use of uninitialized value in concatenation~; # print directly to STDERR, do NOT call warn print STDERR $msg; };
Use print STDERR inside the handler, not warn. Direct output. No recursion.

Part 5: PROMOTING WARNINGS TO FATAL

Sometimes a warning is not just a warning. It is a bug waiting to happen:
$SIG{__WARN__} = sub { my $msg = shift; # these warnings are bugs, not warnings if ($msg =~ m~Use of uninitialized value~) { die "FATAL (promoted from warning): $msg"; } # everything else stays a warning print STDERR $msg; };
Now any use of an uninitialized variable kills the program immediately. Harsh? Yes. But if your code should never operate on undefined values, this catches the bug at the exact point it happens instead of letting it silently produce garbage.

You can also use use warnings FATAL => 'all' to make ALL warnings fatal, but that is a blunt instrument. The $SIG{__WARN__} handler lets you pick exactly which categories get promoted.

# promote specific categories, leave others as warnings $SIG{__WARN__} = sub { my $msg = shift; my @fatal_patterns = ( qr~Use of uninitialized value~, qr~Argument .+ isn't numeric~, qr~Possible attempt to separate words~, ); for my $pat (@fatal_patterns) { if ($msg =~ $pat) { die "PROMOTED: $msg"; } } print STDERR $msg; };

Part 6: COUNTING WARNINGS

Track how many warnings fire and report at exit:
my %warn_counts; $SIG{__WARN__} = sub { my $msg = shift; chomp $msg; # strip the "at file line N" to group duplicates (my $key = $msg) =~ s~ at \S+ line \d+\.?$~~; $warn_counts{$key}++; print STDERR "$msg\n"; }; END { if (%warn_counts) { print STDERR "\n--- Warning Summary ---\n"; for my $msg (sort { $warn_counts{$b} <=> $warn_counts{$a} } keys %warn_counts) { printf STDERR " %4d x %s\n", $warn_counts{$msg}, $msg; } my $total = 0; $total += $_ for values %warn_counts; print STDERR " Total: $total warnings\n"; } }
Now at the end of your program, you get a summary:
--- Warning Summary --- 142 x Use of uninitialized value $name in concatenation 37 x Argument "N/A" isn't numeric in addition 3 x Subroutine process_row redefined Total: 182 warnings
Immediately tells you where the real problems are. The 142 uninitialized values are the priority. The 3 redefinitions are probably harmless.

Part 7: CATEGORY-BASED HANDLING

Perl warnings have categories. You can detect them in the handler and route accordingly:
use warnings; $SIG{__WARN__} = sub { my $msg = shift; if ($msg =~ m~uninitialized~) { log_to_file('undef.log', $msg); } elsif ($msg =~ m~deprecated~i) { log_to_file('deprecations.log', $msg); } elsif ($msg =~ m~redefine~i) { # silently ignore redefinition warnings return; } else { # everything else goes to STDERR print STDERR $msg; } }; sub log_to_file { my ($file, $msg) = @_; if (open my $fh, '>>', "/var/log/myapp/$file") { print $fh $msg; close $fh; } }
Different warning types go to different files. Deprecation warnings pile up in their own log where you can review them at your leisure. Uninitialized value warnings get their own file for focused debugging. Redefinitions are silenced because you know about them already.

Part 8: LOCAL SCOPE WITH LOCAL

You can temporarily replace the warn handler for a specific block using local:
# global handler $SIG{__WARN__} = sub { print STDERR "GLOBAL: @_"; }; warn "this uses the global handler"; { local $SIG{__WARN__} = sub { print STDERR "LOCAL: @_"; }; warn "this uses the local handler"; some_function(); # warnings from here also use the local handler } warn "back to the global handler";
The local keyword saves the current handler and restores it when the block exits. This is perfect for wrapping a noisy library call:
{ # silence warnings from a chatty module local $SIG{__WARN__} = sub { }; NoisyModule::do_stuff(); } # warnings are back to normal here
The handler is dynamically scoped, not lexically scoped. That means it affects all code called from within the block, not just code written inside it. If NoisyModule::do_stuff() calls ten other functions, they all see the silenced handler.

Part 9: PRACTICAL DEBUGGING

A debugging handler that includes a stack trace with every warning:
use Carp qw(longmess); $SIG{__WARN__} = sub { my $msg = shift; chomp $msg; my $trace = longmess("WARNING: $msg"); print STDERR $trace; };
Now instead of:
Use of uninitialized value in addition at process.pl line 42.
You get:
WARNING: Use of uninitialized value in addition at process.pl line 42. at process.pl line 42. main::calculate_total(undef) called at process.pl line 28 main::process_row('HASH(0x...)') called at process.pl line 15 main::main() called at process.pl line 8
Now you know the full call chain. Line 42 is where the warning fired, but the real bug might be at line 28 where undef was passed in, or at line 15 where the data was loaded.

A toned-down version that only adds the immediate caller:

$SIG{__WARN__} = sub { my $msg = shift; chomp $msg; my ($pkg, $file, $line) = caller(1); if (defined $file) { print STDERR "$msg (called from $file:$line)\n"; } else { print STDERR "$msg\n"; } };

Part 10: THE COMPLETE PATTERN

Putting it all together. A production-grade warning handler:
#!/usr/bin/env perl use strict; use warnings; use feature 'say'; use POSIX qw(strftime); # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Warning handler configuration # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ my $log_dir = '/var/log/myapp'; my %warn_counts; my @suppress = ( qr~Subroutine .+ redefined~, ); my @fatal = ( qr~Attempt to free unreferenced scalar~, ); $SIG{__WARN__} = sub { my $msg = shift; chomp $msg; # suppress known noise for my $pat (@suppress) { return if $msg =~ $pat; } # promote critical warnings to fatal for my $pat (@fatal) { die "FATAL (promoted): $msg\n" if $msg =~ $pat; } # count for summary (my $key = $msg) =~ s~ at \S+ line \d+\.?$~~; $warn_counts{$key}++; # log with timestamp my $ts = strftime("%Y-%m-%d %H:%M:%S", localtime); my $line = "[$ts] $msg\n"; if (open my $fh, '>>', "$log_dir/warnings.log") { print $fh $line; close $fh; } print STDERR $line; }; END { return unless %warn_counts; print STDERR "\n--- Warning Summary ---\n"; for my $msg (sort { $warn_counts{$b} <=> $warn_counts{$a} } keys %warn_counts) { printf STDERR " %4d x %s\n", $warn_counts{$msg}, $msg; } }
Filters, timestamps, file logging, counting, and a summary at exit. All from one signal handler.
.--. |o_o | "Your warnings work |:_/ | for you now. // \ \ Not against you." (| | ) /'\_ _/`\ \___)=(___/
Perl does not hide warnings from you. It gives you a hook and says "do whatever you want with these." Most people ignore the hook and let warnings fly to STDERR unprocessed.

That works fine for small scripts. For anything that runs in production, for anything that generates more than a screenful of output, you need control. $SIG{__WARN__} is that control.

Intercept. Filter. Route. Count. Promote. Silence. Whatever the situation demands. The warning system is not just a firehose you point at STDERR. It is a programmable event stream.

Treat it like one.

perl.gg