Perl Part I · Module 14 · Errors, Warnings, and Debugging
Try::Tiny and Modern Error Handling
Chapter 14.4 ended with a working but fussy pattern: an eval block, a trailing 1, a copy of $@ taken immediately, and nothing allowed to run in between. Try::Tiny replaces all of it with try, catch and finally — the same words other languages use, with the same meaning. It is a small module with no dependencies and it removes a whole class of mistake. This chapter measures what it actually fixes rather than asserting it, shows the two genuine surprises in its syntax, and corrects one warning about it that is repeated everywhere and does not survive a measurement.
Advanced13 min readPerlPart IDebugging
Part I · Module 14 · Chapter 14.5 · Try::Tiny and Modern Error Handling
1. The Shape
use strict;
use warnings;
use Try::Tiny;
print "Try::Tiny version $Try::Tiny::VERSION\n\n";
try {
die "bad slack value\n";
}
catch {
my $err = $_; # the error is in $_, not $@
$err =~ s/\s+\z//;
print "catch: \$_ = '$err'\n";
print "catch: \$\@ = '", ($@ // ""), "'\n";
}
finally {
print "finally: ran after the failure\n";
};
print "\n";
try { print "try: the happy path\n" }
catch { print "catch: not reached\n" }
finally{ print "finally: ran after the success\n" };Try::Tiny version 0.30
catch: $_ = 'bad slack value'
catch: $@ = ''
finally: ran after the failure
try: the happy path
finally: ran after the successThree things are measured there:
The error arrives in $_, not $@. This is the single most surprising thing about the module and the thing people trip over. Inside catch, $_ is the error.
$@ is empty inside catch. The module does not leak it. Everything Chapter 14.3 warned about — a stale $@, an erased $@, a copy that must be taken on the very next statement — simply does not arise, because you are not reading a global.
finally ran on both paths. After the failure and after the success. That is what it is for: closing a handle, removing a temporary file, restoring a setting — work that must happen either way.
2. What It Actually Fixes
Chapter 14.4 measured eval { } or do { } firing its error handler when nothing had died, because the block's last value was 0 or "". Here is the same case through try:
use strict;
use warnings;
use Try::Tiny;
# the two values that produced a FALSE ALARM under "eval { } or do { }"
for my $value (0, "") {
my $result = try { $value } catch { "CATCH RAN" };
printf "try returning %-4s -> result '%s' catch ran: %s\n",
"'$value'", $result, ($result eq "CATCH RAN" ? "YES" : "no");
}
# and a real failure, for contrast
my $real = try { die "a real failure\n" } catch { "CATCH RAN" };
printf "try that dies -> result '%s' catch ran: %s\n",
$real, ($real eq "CATCH RAN" ? "YES" : "no");
# the same two values under the 14.4 idiom, for the direct comparison
print "\nthe plain eval idiom, same values:\n";
for my $value (0, "") {
my $ok = eval { $value }; # no trailing 1 -- the 14.4 bug
printf "eval returning %-4s -> 'or do' handler fires: %s\n",
"'$value'", $ok ? "no" : "YES";
}try returning '0' -> result '0' catch ran: no
try returning '' -> result '' catch ran: no
try that dies -> result 'CATCH RAN' catch ran: YES
the plain eval idiom, same values:
eval returning '0' -> 'or do' handler fires: YES
eval returning '' -> 'or do' handler fires: YESThe contrast is exact. try returned 0 and "" as results and never ran catch, while the same two values under the bare eval idiom fired the handler both times. catch runs when something dies, full stop — there is no truth test on your block's return value to get wrong.
So the list of mistakes Try::Tiny makes impossible:
- No trailing
1to remember, because the return value is never used as a success flag. - No
$@to copy, so nothing can erase it between the failure and your handler. - No "did I test it soon enough" question at all.
- An error object that is false stays caught —
catchis entered on death, not on truthiness, which closes the one gap Chapter 14.4 had to mention as an exception.
3. The Two Real Surprises
Both are syntax, and both bite once.
The error is $_. Not $@. If you want a name, assign it on the first line: my $err = $_;. Perl's $_ is also what for, map and grep set, so a loop inside your catch block will overwrite it — copy it first and the problem disappears.
There is a semicolon at the end. Look at the blocks above: they end }; not }. This is not a style choice. try is an ordinary subroutine taking code references, not a keyword, so the whole thing is a function call and needs its statement terminator like any other.
use strict;
use warnings;
use Try::Tiny;
# try/catch is a FUNCTION CALL, which is why the semicolon is required
# and why the blocks can be stored and passed around like any other data
my $result = try { die "x\n" } catch { "recovered" };
print "it is an expression, so it returns a value: '$result'\n";
# proof that these are plain code references being handed to a sub
print "try is a ", ref(\&Try::Tiny::try), ", not a keyword\n";
# which also means this works: assigning the outcome of a whole try/catch
my @parsed = try { (10, 20, 30) } catch { () };
print "and it propagates a list: (", join(", ", @parsed), ")\n";it is an expression, so it returns a value: 'recovered'
try is a CODE, not a keyword
and it propagates a list: (10, 20, 30)Because it is a function call returning a value, try is an expression. You can assign its result, as both those lines do, and it passes a list through unchanged. That is more useful than it sounds — it lets you write my $config = try { load($path) } catch { {} }; and have a sensible default in one statement.
4. The return Gotcha — And The Comparison That Is Wrong
Here is the warning you will read everywhere: return inside a try block does not return from the enclosing subroutine. It returns from the try block, because that block is a subroutine of its own.
That warning is true. But it is nearly always presented as a way Try::Tiny differs from a plain eval, and that part does not survive a measurement.
use strict;
use warnings;
use Try::Tiny;
sub with_try {
try { return "FROM-TRY" } catch { return "FROM-CATCH" };
return "FROM-SUB-END";
}
sub with_eval {
eval { return "FROM-EVAL" };
return "FROM-SUB-END";
}
print "Try::Tiny : ", with_try(), "\n";
print "plain eval: ", with_eval(), "\n";Try::Tiny : FROM-SUB-END
plain eval: FROM-SUB-ENDBoth printed FROM-SUB-END. The return did not escape the block in either case. A plain eval block has exactly the same property, for exactly the same reason, and a return inside it only leaves the eval.
So keep the warning and drop the comparison. The hazard is real and worth watching for; it is simply not a cost of adopting Try::Tiny, because you already had it.
The way to write it safely is to use the value instead of returning it:
use strict;
use warnings;
use Try::Tiny;
# WRONG: the return never leaves the try block
sub bad_load {
my ($path) = @_;
try { return read_it($path) } catch { return "default" };
return "FELL THROUGH";
}
# RIGHT: let try/catch produce the value, then return that
sub good_load {
my ($path) = @_;
my $value = try { read_it($path) } catch { "default" };
return $value;
}
sub read_it {
my ($path) = @_;
die "cannot read $path\n" unless $path eq "good";
return "real contents";
}
for my $p ("good", "missing") {
printf "%-8s bad_load -> %-13s good_load -> %s\n", $p, bad_load($p), good_load($p);
}good bad_load -> FELL THROUGH good_load -> real contents
missing bad_load -> FELL THROUGH good_load -> defaultbad_load returned FELL THROUGH for both inputs — it ignored the real answer and the fallback. good_load returned the right thing in both cases. Note that good_load's catch block has no return in it at all: the block's last value is the result, which is the whole idiom.
5. When Not To Reach For It
Try::Tiny is a dependency, and in this track that is a real consideration — Chapter 12.10 covered why adding one to a script that must run on a locked-down machine is a decision rather than a detail.
Use it when a script or module does real error handling in several places. The readability gain compounds, and the $@ hazards disappear.
Stay with eval for a single guarded operation in a standalone script, especially one you need to run anywhere with no setup. eval { ...; 1 } or do { ... }; is three extra characters of discipline and it is correct.
Check before you assume it is there. It is in no Perl core release, so a script that uses it fails at compile time on a machine without it. Where that matters, the optional-dependency form from Chapter 14.4 applies:
my $have_try = eval { require Try::Tiny; Try::Tiny->import; 1 };6. What To Remember
try/catch/finallyfromTry::Tiny, version 0.30 here, no dependencies.- The error is in
$_insidecatch, not$@. Copy it to a named variable on the first line. - Measured:
$@is empty insidecatch— the module does not leak it, so none of Chapter 14.3's hazards apply. finallyruns on both paths, success and failure. Use it for cleanup.- The whole thing needs a trailing semicolon, because
tryis a function call and not a keyword. - Measured: it fixes 14.4's false alarm.
try { 0 }andtry { "" }returned their values and never enteredcatch, where the bareevalidiom fired the handler for both. - No trailing
1, no$@copy, no timing question — the class of mistake is gone, not merely easier to avoid. tryis an expression. It returns a value and propagates a list, somy $x = try { ... } catch { ... };is the normal way to use it.- Measured: the famous
returngotcha is not a difference fromeval. Both returnedFROM-SUB-END. Keep the warning, drop the comparison. - Do not
returnfrom inside the blocks — lettryandcatchproduce the value and return that. Measured, the wrong form ignored both the answer and the fallback. - It is not core, so guard the
requirewhere the target machine is not yours. - Native
try/catchexists from 5.34 as experimental — worth knowing, not yet worth depending on.
Chapter 14.6 turns the other way. Instead of catching deaths, it makes Perl produce them: inside a chosen block, one category of warning stops the script instead of printing a message — which is what you want when a missing value would otherwise be quietly treated as zero.
Where this fits
Part of the Perl curriculum.
