Flexible exceptions handling with Dancer::Exception
Among the new features added in Dancer, here is an important albeit not very visible one.
When running a Dancer application, sometimes things get wrong, and an exception
is raised. In earlier versions, Dancer was using croak
. That's not bad, but
when trying to catch an exception, it's not trivial to programatically know if
it was raised by Dancer's code, or the web application's code, or some other
module used.
Now, all exceptions raised in Dancer's code are Dancer::Exceptions. It's also possible for the developer of the web application to create and raise their own Dancer::Exception.
Technically, a Dancer's exception is an object inheriting from Dancer::Exception::Base, but may be composed from different roles, enabling different flavours of exceptions. There is one special flavour, called 'Core'. All exceptions raised by Dancer's core code are of this flavour, or derive from it. Dancer users are able to create new flavours, and new exceptions of these flavours.
Catch Dancer's core exceptions
First things first, exceptions means that there is a way to catch them. For
that, Dancer technically relies on the very good Try::Tiny, but provides
itself a slightly modified version of try
and catch
. Just use
Dancer::Exception and you're set.
Try and catch exceptions
use Dancer; use Dancer::Exception qw(:all); get '/' => sub { try { do_some_stuff(); } catch { # ah, something bad happened }; };
Beware, the try
and catch
syntax requires to append a semi-colon ;
at the end.
Core or non core exceptions ?
When catching an exception, one of the first thing to do is testing if it's a
core exception (coming from Dancer's code). To do that, use the does
keyword:
use Dancer; use Dancer::Exception qw(:all); get '/' => sub { try { do_some_stuff(); } catch { my ($exception) = @_; if ($exception->does('Core')) { # issues appeared in Dancer's code say "got a core exception: " . $exception->message; $exception->rethrow; } else # handle the issue } }; };
This example shows the usage of:
- does
used to test the flavour of an exception
- rethrow
if you just caught an exception, but you actually don't want to handle it, just
rethrow
it! It'll still come from the point it was originally emited from. - message
exceptions contain a message, and the <message()> method returns it as a string. However, Dancer's exception objects overload stringification and string comparison just fine, so you can just
print $exception
Create your own exceptions
Dancer::Exception would be of limited use if it were just there to properly raise core exceptions. It is also possible for Dancer web developers to register new exception flavours, raise these exceptions, and catch and test them.
Register your custom exception
Before being able to raise a home made exception, you need to register it. Every exception has a message, but Dancer exceptions actually have a message pattern. Let's see what that means in an example:
use Dancer::Exception qw(:all); register_exception('NoPermission', message_pattern => "not enough permission: %s" );
This will register a new kind of exception, that can be raised if the user tries to access a part of a website he doesn't have permission to.
It's also possible to register new exceptions that are composed from other exceptions. We can consider that the 'NoPermission' exception is too vague, and we need two sub exceptions to specify if the login or the password was wrong:
register_exception('InvalidLogin', message_pattern => "invalid login '%s'" composed_from => [ 'NoPermission' ] ); register_exception('InvalidPassword', message_pattern => "invalid password" composed_from => [ 'NoPermission' ] );
See how we set the message pattern? We want to display the wrong login, but not the wrong password.
Now, take a look at this piece of code:
use Dancer; use Dancer::Exception qw(:all); any ['get', 'post'] => '/login' => sub { try { _login(params->{'login'}, params->{'password'}); } catch { my ($exception) = @_; if ($exception->does('NoPermission')) { # deal with it template 'no permission', { message => $exception->message } } else { # we can't handle this $exception->rethrow; } }; }
What's great is that if the exception was an InvalidLogin
, the message
returnd by <$exception-
message>> will be:
"not enough permission: invalid login 'foo'"
That's because message patterns are called one by one bottom up. But to fully
understand the effect, one must see the function _login
, which shows how to
raise an exception.
Raise a custom exception
Now that we have registered some exceptions, and seen how to catch them, let's
see how to raise them. It's simple, it's done using the raise
keyword:
sub _login { my ($login, $password) = @_; _check($login) or raise InvalidLogin => $login; _check_credential($login,$password) or raise 'InvalidPassword'; }
raise
takes one or two parameters: the name of the exception's flavour, and
the parameter to its message pattern. Here we pass the login to the InvalidLogin
.
Dancer exceptions are not Dancer errors
On common mistake is to think that Dancer exceptions are the same than Dancer errors. They are not. Dancer::Exceptions are thrown when something goes wrong, either in core code, or the application code. They can be used to signify some malfunction, but can be properly handled by the application, or not.
A Dancer::Error can be roughly seen as an object representation of an HTTP error. So it's what you want your application to send to your end users. When a Dancer::Error object is generated, Dancer's workflow is almost at its end. It's possible to add hooks to execute some code before or after a Dancer::Error generation, but it's not handy for exception handling.
You should however note that when a Dancer::Exception is not caught by the application, it'll eventually be caught by Dancer's code internally, and a Dancer::Error will be generated, and sent to the end user.
Conclusion
I think that's a good first demonstration of what Dancer::Exception can do for you. As a bonus, here is the list of core exceptions that Dancer's core code uses, along with their message patterns:
Core 'core - %s' Core::App 'app - %s' Core::Config 'config - %s' Core::Deprecation 'deprecation - %s' Core::Engine 'engine - %s' Core::Factory 'factory - %s' Core::Factory::Hook 'hook - %s' Core::Hook 'hook - %s' Core::Fileutils 'file utils - %s' Core::Handler 'handler - %s' Core::Handler::PSGI 'handler - %s' Core::Plugin 'plugin - %s' Core::Renderer 'renderer - %s' Core::Route 'route - %s' Core::Serializer 'serializer - %s' Core::Template 'template - %s' Core::Session 'session - %s'
So, when a core exception happens in a renderer, you'll see a message like:
core - renderer - the frobnicator didn't frob properly
AUTHOR
Damien Krotkine, <dams@zarb.org>