Nice::Try - A real Try Catch Block Implementation Using Perl Filter
use Nice::Try; print( "Hello, I want to try\n" ); # Try out { print( "this piece of code\n" ); try { # Not so sure } print( "I am trying!\n" ); die( "Bye cruel world..." ); # Never going to reach this return( 1 ); } # Some comment catch( Exception $e ) { return( "Caught an exception $e" ); } # More comment with space too catch( $e ) { print( "Got an error: $e\n" ); } finally { print( "Cleaning up\n" ); } print( "Ok, then\n" );
When run, this would produce, as one would expect:
Hello, I want to try this piece of code I am trying! Got an error: Bye cruel world... at ./some/script.pl line 18. Cleaning up Ok, then
Also since version 1.0.0, Nice::Try is context aware:
use Want; # an awesome module which extends wantarray sub info { my $self = shift( @_ ); try { # Do something if( want('OBJECT') ) { return( $self ); } elsif( want('CODE') ) { # dummy code ref for example return( sub{ return( $name ); } ); } elsif( want('LIST') ) { return( @some_data ); } elsif( want('ARRAY') ) { return( \@some_data ); } elsif( want('HASH') ) { return({ name => $name, location => $city }); } elsif( want('REFSCALAR') ) { return( \$name ); } elsif( want('SCALAR' ) ) { return( $name ); # regular string } elsif( want('VOID') ) { return; } } catch( $e ) { $Logger->( "Caught exception: $e" ); } } # regular string context my $name = $o->info; # code context my $name = $o->info->(); # list context like wantarray my @data = $o->info; # hash context my $name = $o->info->{name}; # array context my $name = $o->info->[2]; # object context my $name = $o->info->another_method; # scalar reference context my $name = ${$o->info};
v1.0.1
Nice::Try is a lightweight implementation of Try-Catch exception trapping block using perl filter. It behaves like you would expect.
Here is a list of its distinctive features:
No routine to import like Nice::Try qw( try catch ). Just add use Nice::Try in your script
Nice::Try qw( try catch )
use Nice::Try
Properly report the right line number for the original error message
Allows embedded try-catch block within try-catch block, such as:
use Nice::Try; print( "Wow, something went awry: ", &gotcha, "\n" ); sub gotcha { print( "Hello, I want to try\n" ); # Try out { CORE::say( 'this piece' ); try { # Not so sure } print( "I am trying!\n" ); try { die( "Bye cruel world..." ); return( 1 ); } catch( $err ) { die( "Dying again with embedded error: '$err'" ); } } catch( Exception $e ) { return( "Caught an exception \$e" ); } catch( $e ) { try { print( "Got an error: $e\n" ); print( "Trying something else.\n" ); die( "No really, dying out... with error: $e\n" ); } catch( $err2 ) { return( "Returning from catch L2 with error '$err2'" ); } } CORE::say( "Ok, then" ); }
No need for semicolon on the last closing brace
It does not rely on perl regular expression, but instead uses PPI (short for "Perl Parsing Interface").
Variable assignment in the catch block works. For example:
try { # Something or die( "Oops\n" ); } catch( $funky_variable_name ) { return( "Oh no: $funky_variable_name" ); }
$@ is always available too
$@
You can return a value from try-catch blocks, even with embedded try-catch blocks
It recognises @_ inside try-catch blocks, so you can do something like:
@_
print( &gotme( 'Jacques' ), "\n" ); sub gotme { try { print( "I am trying my best $_[0]!\n" ); die( "But I failed\n" ); } catch( $some_reason ) { return( "Failed: $some_reason" ); } }
Would produce:
I am trying my best Jacques! Failed: But I failed
try or catch blocks can contain flow control keywords such as next, last and redo
try
catch
next
last
redo
while( defined( my $product = $items->[++$i] ) ) { try { # Do something last if( !$product->active ); } catch( $oops ) { $log->( "Error: $oops" ); last; } } continue { try { if( $product->region eq 'Asia' ) { push( @asia, $product ); } else { next; } } catch( $e ) { $log->( "An unexpected error has occurred. Is $product an object? $e" ); last; } }
Can be used with or without a catch block
Supports a finally block called in void context for cleanup for example
finally
Nice::Try is rich context aware, which means it can provide you with a super granular context on how to return data back to the caller based on the caller's expectation, by using a module like Want.
Call to "caller" in perlfunc will return the correct entry in call stack
#!/usr/bin/perl BEGIN { use strict; use warnings; use Nice::Try; }; { &callme(); } sub callme { try { my @info = caller(1); # or my @info = caller; print( "Called from package $info[0] in file $info[1] at line $info[2]\n" ); } catch( $e ) { print( "Got an error: $e\n" ); } }
WIll yield: Called from package main in file ./test.pl at line 10
Called from package main in file ./test.pl at line 10
There are quite a few implementations of try-catch blocks in perl, and they can be grouped in 4 categories:
For example Try::Tiny
For example Nice::Try, Try::Harder
For example TryCatch
For example Syntax::Keyword::Try and now perl with version 5.33.7 using experimental feature.
Group 1 requires the use of semi-colons like:
try { # Something } catch { # More code };
It also imports the subroutines try and catch in your namespace.
And you cannot do exception variable assignment like catch( $err )
catch( $err )
In group 2, Try::Harder does a very nice work, but relies on perl regular expression with Text::Balanced and that makes it susceptible to failure if the try-catch block is not written as it expects it to be. For example if you put comments between try and catch, it would not work anymore. This is because parsing perl is famously difficult. Also, it does not do exception variable assignment, or catch filtered based on exception class like:
try { # Something die( Exception->new( "Failed!" ) ); } catch( Exception $e ) { # Do something if exception is an Exception class }
See "die" in perlfunc for more information on dying with an object.
Also Try::Harder will die if you use only try with no catch, such as:
use Try::Harder; try { die( "Oops\n" ); } # Will never reach this print( "Got here with $@\n" );
In this example, the print line will never get executed. With Nice::Try you can use try alone as an equivalent of "eval" in perlfunc and the $@ will be available too. So:
use Nice::Try; try { die( "Oops\n" ); } print( "Got here with $@\n" );
will produces:
Got here with Oops
In group 3, TryCatch was working wonderfully, but was relying on Devel::Declare which was doing some esoteric stuff and eventually the version 0.006020 broke TryCatch and there seems to be no intention of correcting this breaking change.
In group 4, there is Syntax::Keyword::Try, which is a great alternative if you do not care about exception class filter (it supports variable assignment since 2020-08-01 with version 0.18).
Although, the following script would not work under Syntax::Keyword::Try :
BEGIN { use strict; use warnings; use Syntax::Keyword::Try; }; { &callme(); } sub callme { try { print( "Hello there\n" ); } catch ($e) { print( "Got an error: $e\n" ); } }
This will trigger the following error:
syntax error at ./test.pl line 18, near ") {" syntax error at ./test.pl line 21, near "}" Execution of ./test.pl aborted due to compilation errors.
That is because Syntax::Keyword::Try expects to be used outside of a BEGIN block like this:
used
use strict; use warnings; use Syntax::Keyword::Try; # Rest of the script, same as above
Of course, with Nice::Try, there is no such constraint. You can "use" in perlfunc Nice::Try inside or outside of a BEGIN block indistinctively.
BEGIN
Since perl version 5.33.7 you can use the try-catch block using an experimental feature which may be removed in future versions, by writing:
use feature 'try'; # will emit a warning this is experimental
This new feature supports try-catch block and variable assignment, but no exception class, nor support for finally block, so you can do:
try { # Oh no! die( "Argh...\n" ); } catch( $oh_well ) { return( $self->error( "Something went awry: $oh_well" ) ); }
But you cannot do:
try { # Oh no! die( MyException->new( "Argh..." ) ); } catch( MyException $oh_well ) { return( $self->error( "Something went awry with MyException: $oh_well" ) ); } # No support for 'finally' yet in perl version 5.33.7 finally { # do some cleanup here }
It is probably a matter of time until this is fully implemented in perl as a regular non-experimental feature.
So, Nice::Try is quite unique and fill the missing features, but because it is purely in perl and not an XS module, it is slower than XS module like Syntax::Keyword::Try. I am not sure the difference would be that noticeable, since the parsing by PPI is now done using an XS module, which makes things very fast.
Like with other language such as Java or JavaScript, the finally block will be executed even if the try or catch block contains a return statement.
This is useful to do some clean-up. For example:
try { # Something worth dying } catch( $e ) { return( "I failed: $e" ); } finally { # Do some mop up # This would be reached even if catch already returned # Putting return statement here does not actually return anything. # This is only for clean-up }
However, because this is designed for clean-up, it is called in void context, so any return statement there will not actually return anything back to the caller.
return
Nice::Try can be used with a single try block which will, in effect, behaves like an eval and the special variable $@ will be available as always.
try { die( "Oh no, something went wrong!\n" ); } print( "Got here with $@\n" );
or even:
try { die( "Oh no, something went wrong!\n" ); } catch( $e ); # Not very meaningful, but it will work print( "Got here with $@\n" );
However, if you decide to catch class exceptions, make sure to add a default catch( $e ). For example:
catch( $e )
try { die( MyException->new( "Oh no" ) ); } print( "Got here with $@\n" );
will work and print will display "Got here with Oh no". However:
print
try { die( MyException->new( "Oh no" ) ); } catch( Some::Exception $e ) { # won't reach here }
will make your process die because of the exception not being caught, thus you might want to do instead:
try { die( MyException->new( "Oh no" ) ); } catch( Some::Exception $e ) { # won't reach here } catch( $default ) { print( "Got you! Error was: $default\n" ); }
And the last catch will catch the exception.
Since, try-catch block can be nested, the following would work too:
try { try { die( MyException->new( "Oh no" ) ); } catch( Some::Exception $e ) { # won't reach here } } catch( MyException $e ) { print( "Got you! MyException was: $e\n" ); } # to play it safe catch( $e ) { # do something about it }
Since version v0.2.0 Nice::Try supports the use of flow control keywords such as next, last and redo inside try-catch blocks. For example:
my @names = qw( John Jack Peter Paul Mark ); for( $i..$#names ) { try { next if( $i == 2 ); # some more code... } catch( $e ) { print( "Got exception: $e\n" ); } }
It also works inside the catch block or inside the continue block:
continue
while( defined( my $product = $items->[++$i] ) ) { # Do something } continue { try { if( $product->region eq 'Asia' ) { push( @asia, $product ); } else { next; } } catch( $e ) { $log->( "An unexpected error has occurred. Is $product an object? $e" ); last; } }
Control flow with labels also work
ELEM: foreach my $n ( @names ) { try { $n->moveAfter( $this ); next ELEM if( $n->value == 1234567 ); } catch( $oops ) { last ELEM; } }
However, if you enclose a try-catch block inside another block, use of next, last or redo will silently not work. This is due to perl control flow. See perlsyn for more information on this. For example, the following would not yield the desired outcome:
ELEM: foreach my $n ( @names ) { { # <--- Here is the culprit try { $n->moveAfter( $this ); # This next statement will not do anything. next ELEM if( $n->value == 1234567 ); } catch( $oops ) { # Neither would this one. last ELEM; } } }
Nice::Try provides a high level of granularity about the context in which your subroutine was called.
Normally, you would write something like this, and it works as always:
sub info { try { # do something here if( wantarray() ) { return( @list_of_values ); } # caller just want a scalar elsif( defined( wantarray() ) ) { return( $name ); } # otherwise if undefined, it means we are called in void context, like: # $o->info; with no expectation of return value } catch( $e ) { print( "Caught an error: $e\n" ); } }
The above is nice, but how do you differentiate cases were your caller wants a simple returned value and the one where the caller wants an object for chaining purpose, or if the caller wants an hash or array reference in return?
For example:
my $val = $o->info->[2]; # wants an array reference my $val = $o->info->{name} # wants an hash reference # etc...
Now, you can do the following:
use Want; # an awesome module which extends wantarray sub info { my $self = shift( @_ ); try { # Do something # # same as wantarray() == 1 if( want('LIST') ) { return( @some_data ); } # same as: if( defined( wantarray() ) && !wantarray() ) elsif( want('SCALAR' ) ) { return( $name ); # regular string } # same as if( !defined( wantarray() ) ) elsif( want('VOID') ) { return; } # For the other contexts below, wantarray is of no help if( want('OBJECT') ) { return( $obj ); # useful for chaining } elsif( want('CODE') ) { # dummy code ref for example return( sub{ return( $name ); } ); } elsif( want('ARRAY') ) { return( \@some_data ); } elsif( want('HASH') ) { return({ name => $name, location => $city }); } } catch( $e ) { $Logger->( "Caught exception: $e" ); } }
Thus this is particularly useful if, for example, you want to differentiate if the caller just wants a return string, or an object for chaining.
"wantarray" in perlfunc would not know the difference, and other try-catch implementation would not let you benefit from using Want.
my $val = $o->info; # simple regular scalar context; but... # here, we are called in object context and wantarray is of no help to tell the difference my $val = $o->info->another_method;
Other cases are:
# regular string context my $name = $o->info; # list context like wantarray my @data = $o->info; # code context my $name = $o->info->(); # hash context my $name = $o->info->{name}; # array context my $name = $o->info->[2]; # object context my $name = $o->info->another_method;
See Want for more information on how you can benefit from it.
Currently lvalues are no implemented and will be in future releases. Also note that Want does not work with tied object and overloaded classes. Nice::Try detects this and disable automatically support for Want if the object is overloaded.
Also, for this rich context awareness to be used, obviously try-catch would need to be inside a subroutine, otherwise there is no rich context other than the one the regular "wantarray" in perlfunc provides.
And to have Nice::Try save the filtered code to a file, pass it the debug_file parameter like this:
debug_file
use Nice::Try debug_file => './updated_script.pl';
You can also call your script using Filter::ExtractSource like this:
perl -MFilter::ExtractSource script.pl > updated_script.pl
or add use Filter::ExtractSource inside it.
use Filter::ExtractSource
In the updated script produced, you can add the line calling Nice::Try to:
use Nice::Try no_filter => 1;
to avoid Nice::Try from filtering your script
If you want Nice::Try to produce human readable code, pass it the debug_code parameter like this:
debug_code
use Nice::Try debug_code => 1;
Credits to Stephen R. Scaffidi for his implementation of Try::Harder from which I borrowed some code.
Jacques Deguest <jack@deguest.jp>
PPI, Filter::Util::Call, Try::Harder, Syntax::Keyword::Try, Exception::Class
Copyright (c) 2020-2021 DEGUEST Pte. Ltd.
You can use, copy, modify and redistribute this package and associated files under the same terms as Perl itself.
To install Nice::Try, copy and paste the appropriate command in to your terminal.
cpanm
cpanm Nice::Try
CPAN shell
perl -MCPAN -e shell install Nice::Try
For more information on module installation, please visit the detailed CPAN module installation guide.