[Dancer-users] Dancer::Exception
Tim King
timk at jtimothyking.com
Thu Aug 11 23:24:09 CEST 2011
Hi, Damien. I have lots of comments on this issue, as I've just
implemented an exception-handling mechanism for the project I'm working
on. Some of the features you're considering could have made my job a lot
easier.
CATCHING EXCEPTIONS
In the project that I'm working on, it would have been useful from a
plugin to catch an exception thrown in a route handler. A
"catch_exception" handler might have worked. But this isn't really a
hook, because if a "catch_exception" handler consumes an exception, none
on the other handlers in the chain should be called. It seems to me,
what we're talking about here is a new concept: a "catch-point." It's
like a hook in that it calls back to a handler sub, but it more closely
mirrors try-catch semantics.
As you suggested, someone might also want to catch exceptions from other
parts of the process, e.g., exceptions thrown during deserialization or
serialization, or from within a hook. So it seems that there are *at
least* two distinct points at which applications and plugins might want
to catch exceptions:
* One is in $route->execute. A handler hooked into this catch-point
would execute in the context of the handler, and could do anything the
handler could do, including modify the response, send errors, return
serializable data, and throw the same or different exceptions.
* The other is in Dancer::Handler::render_request. A handler hooked
into this catch-point would be able to modify the response, including
sending errors, but would not have access to the serializer or router.
This handler should also be able to throw the same or other exceptions,
to be caught by later handlers in the chain.
And they may want to catch non-Dancer exceptions, too. Just as Dancer
has default handling in Dancer::Handler::render_request for non-Dancer
exceptions, so also should any catch-point mechanism treat both Dancer
and non-Dancer exceptions equally.
OTHER IDEAS
Idea 2, try-catch route syntax, doesn't add any capability that wasn't
implementable before. Might be nice for pretty syntax, but I don't
particularly care for it. I'd rather just use Try::Tiny, then in my
route handler:
try {
do_some_stuff;
} catch {
if (my $e_value = is_dancer_exception(my $e = $_)) {
handle_dancer_exception($e, $e_value);
} else {
die $e; # rethrow exception
}
};
Idea 3, using the before_error hook, doesn't really seem like the right
solution, because this is about catching exceptions, not about
generating errors. Yes, by default, Dancer instantiates and renders an
error on a caught exception, but there's no particular reason why that's
the only way to handle an exception. In some cases, for example, the
right way to respond to a particular exception might be to forward to a
different path. Or to generate a non-error response.
And using shared data to communicate the exception is so very, very
wrong. As you may guess, I have strong feelings about this, because
Dancer's use of global data has caused me no end of grief, and hack
after hack after hack in my current project, and has only served to
remind me why non-constant global data is evil, evil, evil. Any data
needed to implement Dancer's pretty syntax should be *localized* data
(not global), as Plack uses (for example) in its pretty-syntax
implementation. This is actually a discussion item from a much larger
list I'm compiling, to be published later, and it has little to do with
Dancer::Exception, except that you mentioned it. For now, I'll just say
I cannot urge you strongly enough to *avoid* adding anything to
Dancer::SharedData (or any other global data in the system, such as in
Dancer::Serializer), and instead consider how to refactor the global
data that's already there in order to limit its scope or make it more
object-oriented.
THE SESSION SYSTEM EXAMPLE
I haven't used the Dancer session system, and I'm using the latest
released Dancer (not the devel branch). So I can't really knowledgeably
comment on "Real life example 3." But it seems to me that this is a
situation in which you'd want to use a meta-session class, analogous to
Dancer::Serializer::Mutable. That is, the mutable serializer chooses
which serializer to use— in that case, based on the content types that
the request and the serializers support. Then
Dancer::Serializer::Mutable proxies the API to that chosen serializer.
One might visualize a session meta-engine that tries each configured
session engine, handing off control to that engine. As each engine threw
E_SESSION, the meta-engine would catch it, trying each additional engine
in turn, until it found one that accepted the request. The meta-engine
itself would only raise E_SESSION when none of the configured engines
could handle the request.
The meta-engine (if possible) could even cache the knowledge it learned
from these trials, so that it wouldn't have to go through multiple
trials and failures for each and every request. That sounds very
inefficient, multiple trials and failures every single request, for
normal, expected requests. It should have some way of knowing which
engine is to handle a given request, and funnel that request to that
engine. Only when an engine becomes inoperative (e.g., because a
database has gone down) should it search for an alternative.
In particular, you probably don't want to implement this functionality
by catching the E_SESSION exceptions at a higher level. Because its
logic is local to the session subsystem. Keep things that work together
close together; keep things that work separately apart.
However, if there is a compelling reason to catch exceptions at some
other point in the Dancer framework, you could define additional
catch-points.
DANCER EXCEPTIONS
Apps may want to throw other exception objects, not just internal Dancer
exceptions. And it's important to keep that in mind. Because
Dancer::Exception is of limited usefulness to many apps.
Frankly, I'm still stymied by the choice to use a power-of-2 integer as
an exception code in Dancer::Exception. It's no more lightweight than
using an arbitrary integer—in fact, it's slightly heavier, as seen
below—and it restricts the usefulness of the class.
The reason to use a power-of-2 integer is if you have a small, limited
number of defined conditions, and you want to communicate two or more of
them simultaneously in the same value. So for example:
use Fcntl ':mode';
$mode = (stat($filename))[2];
print "$filename is readable\n" if ($mode & S_IREAD);
print "$filename is a directory\n" if ($mode & S_IFDIR);
In this example, the mode returned by stat is a bit-mask simultaneously
communicating the state of all the different file permissions that stat
knows about, as they pertain to the $filename being examined.
But exceptions are a horse of a different color... or maybe an animal of
a different species. You only raise one exception at a time, and there
can be an arbitrarily large set of possible exceptions that an app might
throw. Looking at the latest released Dancer source, I see that
Dancer::Exception will refuse even to register more than 16 exceptions—
on a system with 64-bit integers. That means you're limited to a maximum
of 16 different exception types, and of the 64 bits carried around with
each exception object, 48 of them will always go to waste. That's
heavier (or less useful) than using an arbitrary integer value.
While Dancer::Exception might still be useable for internal Dancer
exceptions, it fails to provide the minimum feature-set needed for most
application code. In general, a lightweight exception class should
encompass a simple integer value and a string, and it should be
subclassable for apps that need to extend it... Of course, what I've
just described is Exception::Base, which is why I've been throwing
Exception::Base objects in my code.
Anyhow, you asked for feedback. Hope you weren't disappointed. :)
Cheers,
-TimK
http://www.JTimothyKing.com/
On 8/8/11 11:05 AM, damien krotkine wrote:
> Hi,
>
> I'm in the midle of doing the second round coding for
> Dancer::Exception (it's in topic/exceptions2 on github). I have few
> design questions, and I'd like some feedback from the community.
>
> The plan is that with Dancer::Exception :
> - 1/ an exception has a value ( combination of power of 2 integers),
> and a message (string)
> - 2/ all exceptions raised by the Dancer core will be
> Dancer::Exception (instead of normal die / croak)
> - 3/ a user will be able to raise exceptions in the routes
> - 4/ a user will be able to create new exception types
> - 5/ when raised, exceptions are transmitted to the templating system
> when rendering a Dancer::Error
> - 6/ exceptions should properly stringify and numerify for backward
> compatibility and ease of use.
> - 7/ the user should have the opportunity to catch any Dancer
> exception and perform an appropriate action
>
> Currently, in the topic/exceptions2 branch, all items are implemented,
> except item 7. More precisely, it's possible to catch
> Dancer::Exception using standard eval {...}. But what if a user wants
> to catch an exception raised from a distant route, or from Dancer Core
> itself ?
>
> Real life example 1 : in some routes, the developer does some 'raise'
> of exceptions (internal or custom). The developer wants to run
> specific codes when these exception are raised. For now, he can wrap
> his route code with eval {} and use is_dancer_exception, as stated in
> Dancer::Exception.
>
> Real life example 2 : in some routes, the developper wants to catch
> core exceptions that could occur.
>
> Real life example 3 : if something wrong happens in the
> Dancer::Session::Yaml modules, it raises a E_SESSION exception. When
> this exception occurs, it'll render a Dancer::Error, stating that thet
> exception were a E_SESSION. However what would be cool is that you
> could catch the exceptions, see that it's of type SESSION, and in this
> case try a different Dancer::Session system. In this case the
> exception is a core one.
>
> example 3 is not related to routes, so it's a bit difficult to know
> where to put a try/catch scope.
>
> I have some ideas, but I'm not really convinced by them :
> - idea 1 : add a new hook, that would be called 'after_exception'
> - idea 2 : when an exception is raised, it's stored as Shared Data,
> and thus can be detected with a before_error hook
> - idea 3 : add a try_catch syntax when creating route, like :
>
> get '/foo' => sub { ... raise E_LOGIN, "bad credential" },
> catch E_REQUEST | E_GENERIC => sub { ... }
> catch E_LOGIN => sub { ... }
>
> Currently, Dancer::Exception is not too bad : it's simple, efficient
> and fast. I don't want it to become a bloated system. But we need to
> provide a bit more feature for it to be really useful.
>
> Please share your thoughts :)
>
>
> dams.
>
>
>
> _______________________________________________
> Dancer-users mailing list
> Dancer-users at perldancer.org
> http://www.backup-manager.org/cgi-bin/listinfo/dancer-users
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://www.backup-manager.org/pipermail/dancer-users/attachments/20110811/8e7b564e/attachment-0001.htm>
More information about the Dancer-users
mailing list