This is the 2nd attempt, at Stas' request I'm pasting the whole thing
here so you can reply and/or edit.
=head1 Exception Handling for mod_perl
Provided here are some guidelines for S<clean(er)> exception handling
for mod_perl usage, although the technique presented here applies to
all of your perl programming.
The reasoning behind this document is the current broken status of
C<$SIG{__DIE__}> in the perl core - see both the perl5-porters and
mod_perl mailing list archives for details on this discussion.
=head1 Trapping Exceptions in Perl
To trap an exception in Perl we use the C<eval{}> construct. Many
people initially make the mistake that this is the same as the C<eval
EXPR> construct, which compiles and executes code at run time, but
that's not the case. C<eval{}> compiles at compile time, just like the
rest of your code, and has next to zero run-time penalty.
When in an eval block, if the code executing die()'s for some reason,
rather than terminating your code, the exception is I<caught> and the
program is allowed to examine that exception and make decisions based
on it. The full construct looks like this:
eval
{
# Some code here
}; # Note important semi-colon there
if ($@) # $@ contains the exception that was thrown
{
# Do something with the exception
}
else # optional
{
# No exception was thrown
}
Most of the time when you see these exception handlers there is no else
block, because it tends to be OK if the code didn't throw an exception.
=head1 Alternative Exception Handling Techniques
An often suggested method for handling global exceptions in mod_perl,
and other perl programs in general, is a B<__DIE__> handler, which can
be setup by either assigning a function name as a string to
C<$SIG{__DIE__}> (not particularly recommended) or assigning a code-ref
to C<$SIG{__DIE__}>, the usual way of doing so is to use an anonymous
sub:
$SIG{__DIE__} = sub { print "Eek - we died with:\n", $_[0]; };
The current problem with this is that C<$SIG{__DIE__}> is a global
setting in your script, so while you can potentially hide away your
exceptions in some external module, the execution of C<$SIG{__DIE__}>
is fairly magical, and interferes not just with your code, but with all
code in every module you import. Beyond the magic involved,
C<$SIG{__DIE__}> actually interferes with perl's normal exception
handling mechanism, the C<eval{}> construct. Witness:
$SIG{__DIE__} = sub { print "handler\n"; };
eval {
print "In eval\n";
die "Failed for some reason\n";
};
if ($@) {
print "Caught exception: $@";
}
The code unfortunately prints out:
In eval
handler
Which isn't quite what you would expect, especially if that
C<$SIG{__DIE__}> handler is hidden away deep in some other module that
you didn't know about. There are work arounds however. One is to
localise C<$SIG{__DIE__}> in every exception trap you write:
eval {
local $SIG{__DIE__};
...
};
Obviously this just doesn't scale - you don't want to be doing that for
every exception trap in your code, and it's a slow down. A second work
around is to check in your handler if you are trying to catch this
exception:
$SIG{__DIE__} = sub {
die $_[0] if $^S;
print "handler\n";
};
However this won't work under Apache::Registry - you're always in an
eval block there!
My personal solution is to warn people about this danger of
C<$SIG{__DIE__}> and inform them of better ways to code. This is my
attempt at that.
=head1 Better Exception Handling
The C<eval{}> construct in itself is a fairly weak way to handle
exceptions as strings. There's no way to pass more information in your
exception, so you have to handle your exception in more than one place
- at the location the error occured, in order to construct a sensible
error message, and again in your exception handler to deconstruct that
string into something meaningful (unless of course all you want your
exception handler to do is dump the error to the browser).
A little known fact about exceptions in perl 5.005 is that you can call
die with an object. The exception handler receives that object in $@.
This is how I always handle exceptions now, as it provides an extremely
flexible and scaleable exceptions solution.
=head2 A Little Housekeeping
First though, before we delve into the details, a little housekeeping
is in order. Most, if not all, mod_perl programs consist of a main
routine that is entered, and then dispatches itself to a routine
depending on the parameters passed and/or the form values. In a normal
C program this is your main() function, in a mod_perl handler this is
your handler() function/method.
In order for you to be able to use exception handling to it's best
extent you need to change your script to have some sort of global
exception handling. This is much more trivial than it sounds. If you're
using Apache::Registry to emulate CGI you might consider wrapping your
entire script in one big eval block, but I would discourage that. A
better method would be to modularise your script into discreet function
calls, one of which should be a dispatch routine:
#!/usr/bin/perl -w
# Apache::Registry script
eval {
dispatch();
};
catch($@);
sub dispatch {
...
}
sub catch {
my $exception = shift;
...
}
This is easier with an ordinary mod_perl handler as it is natural to
have separate functions, rather than a long run-on script:
# MyHandler.pm
sub handler {
my $r = shift;
eval {
dispatch($r);
};
catch ($@);
}
sub dispatch {
my $r = shift;
...
}
sub catch {
my $exception = shift;
...
}
Now that the skeleton code is setup, let's create an exception class,
making use of Perl 5.005's ability to throw exception objects.
=head2 An Exception Class
This is a really simple exception class, that does nothing but contain
information. A better implementation would probably also handle its own
exception conditions, but that would be more complex, requiring
separate packages for each exception type.
package My::Exception;
sub AUTOLOAD {
my ($package, $filename, $line) = caller;
no strict 'refs', 'subs';
if ($AUTOLOAD =~ /.*::([A-Z]\w+)$/) {
my $exception = $1;
*{$AUTOLOAD} =
sub {
shift;
push @_, caller => {
package => $package,
filename => $filename,
line => $line,
};
bless { @_ }, "My::Exception::$exception";
};
goto &{$AUTOLOAD};
}
else {
die "No such exception class: $AUTOLOAD\n";
}
}
1;
OK, so this is all highly magical, but what does it do? It creates a
simple package that we can import and use as follows:
use My::Exception;
die My::Exception->SomeException( l_u_e => 42 );
The exception class tracks exactly where we died from using the
caller() mechanism, it also caches exception classes so that
C<AUTOLOAD> is only called the first time an exception of a particular
type is thrown (particularly relevant under mod_perl).
=head1 Catching Uncaught Exceptions
What about exceptions that are thrown outside of your control? We can
fix this using one of 2 possible methods. The first is to override die
globally using the old magical C<$SIG{__DIE__}>, and the second, is the
cleaner non-magical method of overriding the global die() method to
your own die() method that throws an exception that makes sense to your
application.
=head2 Using C<$SIG{__DIE__}>
Overloading using C<$SIG{__DIE__}> in this case is rather simple,
here's some code:
$SIG{__DIE__} = sub {
my $err = shift;
if(!ref $err) {
$err = My::Exception->UnCaught(text => $err);
}
die $err;
};
All this does is catch your exception and rethrow it. It's not as
dangerous as we stated earlier that C<$SIG{__DIE__}> can be, because
we're actually re-throwing the exception, rather than catching it and
stopping there.
There's only one slight buggette left, and that's if some external code
die'ing catches the exception and tries to do string comparisons on the
exception, as in:
eval {
... # some code
die "FATAL ERROR!\n";
};
if ($@) {
if ($@ =~ /^FATAL ERROR/) {
die $@;
}
}
In order to deal with this, we can overload stringification for our
My::Exception::UnCaught class:
{
package My::Exception::UnCaught;
use overload '""' => \&str;
sub str {
shift->{text};
}
}
We can now let other code happily continue.
So what if we don't want to touch C<$SIG{__DIE__}> at all? We can
overcome this by overriding the core die function. This is slightly
more complex than implementing a C<$SIG{__DIE__}> handler, but is far
less magical, and is the right thing to do, according to the
perl5-porters mailing list.
=head2 Using overridden C<CORE::die>
Overriding core functions has to be done from an external
package/module. So we're going to add that to our My::Exception module.
Here's the relevant parts:
use vars qw/@ISA @EXPORT/;
use Exporter;
@EXPORT = qw/die/;
@ISA = 'Exporter';
sub import {
my $pkg = shift;
$pkg->export('CORE::GLOBAL', 'die');
Exporter::import($pkg,@_);
}
sub die {
if (!ref($_[0])) {
CORE::die My::Exception->UnCaught(text => join('', @_));
}
CORE::die $_[0];
}
That wasn't so bad, was it? We're relying on Exporter's export function
to do the hard work for us, exporting the die function into the
CORE::GLOBAL namespace. Along with the above overloaded
stringification, we now have a complete exception system (well, mostly
complete. Exception die-hards would argue that there's no "finally"
clause, and no exception stack, but that's another topic for another
time).
=head1 Some Uses
I'm going to come right out and say now: I abuse this system horribly!
I throw exceptions all over my code, not because I've hit an
exceptional bit of code, but because I want to get straight back out of
the current function, without having to have every single level of
function call check error codes. One way I use this is to return Apache
return codes:
# paranoid security check
die My::Exception->RetCode(code => 204);
Returns a 204 error code (HTTP_NO_CONTENT), which is caught at my top
level exception handler:
if ($@->isa('My::Exception::RetCode')) {
return $@->{code};
}
That last return statement is in my handler() method, so that's the
return code that Apache actually sends. I have other exception handlers
in place for sending Basic Authentication headers and Redirect headers
out. I also have a generic My::Exception::OK class, which gives me a
way to back out completely from where I am, but register that as an OK
thing to do.
Why do I go to these extents? After all, code like slashcode (the code
behind slashdot) doesn't need this sort of thing, so why should my web
site? Well it's just a matter of scaleability and programmer style
really. Should you ever have the misfortune to ever need/want to use
the slashdot code, you'll be in for a big surprise - it's an
unmaintainable nightmare, brought about by ad-hoc development and a
naive first-time perl programmer. Same goes for an awful lot of other
web applications out there.
=head1 Conclusions
Here I've demonstrated a simple and scaleable (and useful) exception
handling mechanism, that fits perfectly with your current code, and
provides the programmer with excellent means to determine what has
happened in his code. Some users might be worried about the overhead of
such code. However in use I've found accessing the database to be a
much more significant overhead, and this is used in some code
delivering to thousands of users.
For similar exception handling techniques, see the Try module, the
Exception module and the Error module, all on CPAN.
=head1 The My::Exception class in its entirity
package My::Exception
use vars qw/@ISA @EXPORT $AUTOLOAD/;
use Exporter;
@ISA = 'Exporter';
@EXPORT = qw/die/;
sub import {
my $pkg = shift;
$pkg->export('CORE::GLOBAL', 'die');
Exporter::import($pkg,@_);
}
sub die {
if (!ref($_[0])) {
CORE::die My::Exception->UnCaught(text => join('', @_));
}
CORE::die $_[0];
}
{
package My::Exception::UnCaught;
use overload '""' => \&str;
sub str {
shift->{text};
}
}
sub AUTOLOAD {
no strict 'refs', 'subs';
if ($AUTOLOAD =~ /.*::([A-Z]\w+)$/) {
my $exception = $1;
*{$AUTOLOAD} =
sub {
shift;
my ($package, $filename, $line) = caller;
push @_, caller => {
package => $package,
filename => $filename,
line => $line,
};
bless { @_ }, "My::Exception::$exception";
};
goto &{$AUTOLOAD};
}
else {
die "No such exception class: $AUTOLOAD\n";
}
}
1;
=head1 See Also
Some users might find it very useful to have a more C++/Java like
interface of try/catch functions. These are available in several forms
that all work in slightly different ways. See the documentation for
each module for details:
L<Error> - Graham Barr's excellent OO try/catch/finally module.
L<Exceptions> - Another exceptions module.
L<Try> - Tony Olekshy's. Adds an unwind stack. Not on CPAN (yet?).
--
<Matt/>
Fastnet Software Ltd. High Performance Web Specialists
Providing mod_perl, XML, Sybase and Oracle solutions
Email for training and consultancy availability.
http://sergeant.org http://xml.sergeant.org