Print debugging, warn, and the Carp family#
This chapter covers non-interactive diagnostics: inserted prints, variable dumps, stack traces from inside the program. These techniques remain the fastest path to understanding “what does my code actually see here?” for bugs that are cheap to reproduce.
Readers who already know about print
and warn will find here the
structured variants that earn their keep: the Carp caller-reporting
family, Data::Dumper and its modern replacements, and the
stack-trace-everywhere one-liners.
warn vs print — pick the right stream#
warn writes to STDERR;
print defaults to STDOUT. Use
warn for diagnostics: it keeps program output clean, survives
STDOUT redirection, and is the stream loggers already capture.
warn "*** foo=[$foo] bar=[@bar]\n";
Wrap values in literal brackets so trailing whitespace and
newlines are visible in the output. "foo=[$foo]" reveals
foo=[ ] where "foo=$foo" just shows foo= .
Without a trailing \n, warn (and die) append at FILE line N.
from the caller’s perspective. With \n, they do not. Rule of
thumb: append \n for user-facing messages; omit it for
developer diagnostics where you want location.
Cheap location tags from the compiler:
warn "reached $0 " . __FILE__ . ':' . __LINE__ . "\n";
warn "in @{[__SUB__->name]}\n"; # 5.16+
The Carp family — report from the caller#
When a library function fails, pointing at the library’s own
source is almost never useful. Carp’s family reports from the
caller’s frame:
Function |
Reports from |
Fatal? |
Stack trace? |
|---|---|---|---|
|
caller |
no |
no |
|
caller |
yes |
no |
|
caller |
no |
full |
|
caller |
yes |
full |
use Carp;
sub open_config {
my $path = shift // croak "open_config: missing path";
open my $fh, '<', $path or croak "cannot read $path: $!";
return $fh;
}
Library code should use croak/carp, not die/warn — the
error message points at the caller’s line, which is where the fix
goes.
Stack traces without touching the code#
Carp::Always installs $SIG{__DIE__} and $SIG{__WARN__} to
route through Carp::confess / Carp::cluck. Run a misbehaving
script with:
perl -MCarp::Always script.pl
Every die and warn produces a full stack. No source changes.
This is the first thing to reach for when a crash gives a useless
one-line message.
Devel::Confess is a drop-in equivalent with fewer edge cases
around overloaded objects:
perl -d:Confess script.pl
Both modules add cost to every warn and die; use them while
investigating, not in steady production.
For a permanent in-code install:
use Carp;
$SIG{__DIE__} = sub { return if $^S; Carp::confess(@_) };
$SIG{__WARN__} = sub { Carp::cluck(@_) };
return if $^S; — “return if we are inside eval”. Without this
guard every caught exception pays the trace cost. See
exceptions for the full $SIG{__DIE__} pattern.
Dumping data structures#
Pass a reference to every dumper; dumping a bare hash or array prints its flat-list form and loses the structure.
Data::Dumper — universal, core, eval-roundtrippable#
use Data::Dumper;
$Data::Dumper::Sortkeys = 1;
$Data::Dumper::Indent = 1;
$Data::Dumper::Terse = 1;
$Data::Dumper::Deepcopy = 1;
warn Dumper(\%config);
Sortkeysmakes output diff-stable.Indent=1is the readable setting (0 = flat, 2 = default, 3 = with array indices).Terse=1suppresses$VAR1 =prefixes when you do not need round-trip-ability.Deepcopy=1disables the “seen this ref before”$VAR1->{...}tracking — prints the data, not a graph reconstruction.
Data::Dumper::Concise is a drop-in with sensible defaults
pre-applied; the DBIx::Class / Moose community standard.
Data::Printer — coloured, modern, readable#
use DDP;
p %config; # not a reference — DDP handles both
Data::Printer prints better-looking output than Data::Dumper,
colours by type, handles objects specially (class, attributes,
overloads), and does not try to round-trip. Prefer it when the
goal is reading the dump, not re-ingesting it.
Data::Dump — compact one-liner#
use Data::Dump qw(dd);
dd \%config; # prints to STDERR in compact form
Gisle Aas’s Data::Dump. Useful when the default Data::Dumper
output is too verbose for a single warn line.
When to reach for which#
Data::Dumperwhen you might feed the output back intoevalor compare two dumps.Data::Printer(DDP) when a human is reading the output.Data::Dumpwhen you want a one-line summary.Data::Dumper::Conciseon projects already using it.
Conditional debug prints#
A compile-time constant is optimised out entirely when false:
use constant DEBUG => 0;
warn "state=$state\n" if DEBUG;
With DEBUG => 0, the if DEBUG test is folded at compile time
and the warn never appears in the op tree — zero runtime cost.
Wire to the environment so the flag is flippable without editing:
use constant DEBUG => $ENV{APP_DEBUG} // 0;
Multiple channels via bitmask:
use constant { WEB => 1, SQL => 2, REGEX => 4 };
my $DEBUG = $ENV{APP_DEBUG} // 0;
sub dbg { my $c = shift; warn "[DBG] @_\n" if $DEBUG & $c }
dbg WEB, "request $req_id start\n";
dbg SQL, "query: $sql\n";
dbg REGEX, "matched: $&\n"; # (unless you care about perf — see below)
Source-filter alternative: Smart::Comments#
use Smart::Comments;
my @stuff = compute();
### @stuff # prints: @stuff: [...]
### Checking key: $key # print, plus asserts $key is truthy
### Processing |=| ... for @stuff
Lines beginning ### are rewritten into prints (and, with
special syntax, progress bars and assertions). The module is a
source filter — it rewrites the program text before compile — so
reported line numbers can drift by one. When not imported, the
### comments compile to nothing. The drift is the price of not
cluttering the code with explicit warn lines.
Taint and $& — incidental costs to know about#
Referencing $&, $`, or $' anywhere in the program
forces perl to maintain these match-context globals for every
regex match in the entire program, including matches inside
loaded modules. Historically this imposed a noticeable slowdown;
on modern perls the cost is smaller but non-zero, and the rule
still applies: do not touch them if you can avoid it.
Use @- / @+ (match position arrays), named captures, or the
/p modifier with ${^MATCH}, ${^PREMATCH}, ${^POSTMATCH}
instead. The /p variants impose the cost only for matches that
opt in.
Timestamped logging#
Hand-rolled warn with localtime is adequate for one-off
investigations:
use Time::HiRes qw(gettimeofday);
sub t { my ($s, $us) = gettimeofday; sprintf "%d.%06d", $s, $us }
warn sprintf "[%s] state=%s\n", t(), $state;
For anything more structured, use a logging framework rather than rolling your own:
Log::Any— library-safe; emit logs without picking a backend. Pair with an adapter (Log::Any::Adapter::Log4perl,::Stdout,::Syslog) in the application.Log::Log4perl— category / level / appender / layout; config file-driven.Log::Dispatch— lower-level router; often used underLog::Any.
Standard levels, lowest to highest: TRACE, DEBUG, INFO,
WARN, ERROR, FATAL. Gate TRACE / DEBUG behind an env
flag in production.
Find out more#
exceptions —
die,$@,$SIG{__DIE__}, typed exception classes, the moderntry/catch.interactive-debugger — when inserting prints costs more than launching
perl -d.tracing — full-program line-by-line trace without editing the source.