The attached patch + test has an attempt at this. Two TODO notes remain:
* Should we provide an API for callers to test "is this value a Future-wrapped exception?"
* Should we provide an API for callers to generate one of these exception objects, other than the side-effect of eval { Future->fail(...)->get }; $@
In addition, the documentation is deliberately vague on the subject of what package these exception objects are blessed into. Given the uses of the "category" name for exception dispatch (not that perl strictly /has/ conditional exception but folks still like to try), it might be useful to somehow encode the category name into the class (which suggests we'll dynamically generate them, hrm) so folks can continue to do that sort of thing. Somehow.
--
Paul Evans
=== modified file 'lib/Future.pm'
--- lib/Future.pm 2019-04-30 14:54:57 +0000
+++ lib/Future.pm 2019-04-30 15:23:26 +0000
@@ -627,12 +627,20 @@
The exception must evaluate as a true value; false exceptions are not allowed.
A failure category name and other further details may be provided that will be
-returned by the C<failure> method in list context. These details will not be
-part of the exception string raised by C<get>.
+returned by the C<failure> method in list context.
If the future is already cancelled, this request is ignored. If the future is
already complete with a result or a failure, an exception is thrown.
+If invoked on a wrapped exception object (i.e. an object previously thrown by
+the C<get>), the additional details will be preserved. This allows the
+additional details to be transparently preserved by such code as
+
+ ...
+ catch {
+ return Future->fail($@);
+ }
+
=cut
sub fail
@@ -640,7 +648,12 @@
my $self = shift;
my ( $exception, @more ) = @_;
- $_[0] or Carp::croak "$self ->fail requires an exception that is true";
+ if( ref $exception eq "Future::_Exception" ) {
+ @more = ( $exception->category, $exception->details );
+ $exception = $exception->message;
+ }
+
+ $exception or Carp::croak "$self ->fail requires an exception that is true";
if( ref $self ) {
$self->{cancelled} and return $self;
@@ -794,7 +807,9 @@
scalar context it returns just the first result value.
If the future is ready but failed, this method raises as an exception the
-failure string or object that was given to the C<fail> method.
+failure that was given to the C<fail> method. If additional details were given
+to the C<fail> method, an exception object is constructed to wrap them. See
+L</EXCEPTION OBJECTS> below.
If the future was cancelled an exception is thrown.
@@ -815,9 +830,10 @@
{
my $self = shift;
until( $self->{ready} ) { $self->await }
- if( $self->{failure} ) {
+ if( my $failure = $self->{failure} ) {
$self->{reported} = 1;
- my $exception = $self->{failure}->[0];
+ my $exception = $failure->[0];
+ $exception = Future::_Exception->new( @$failure ) if @$failure > 1;
!ref $exception && $exception =~ m/\n$/ ? CORE::die $exception : Carp::croak $exception;
}
$self->{cancelled} and Carp::croak "${\$self->__selfstr} was cancelled";
@@ -2171,6 +2187,34 @@
return $cb;
}
+=head1 EXCEPTION OBJECTS
+
+In order to represent the category name and addition details of a failed
+C<Future> instance, the C<get> method will throw an exception values that is
+an object reference with the following methods:
+
+ $message = $e->message
+ $category = $e->category
+ @details = $e->details
+
+Additionally, the object will stringify to return the message value, for the
+common use-case of printing, regexp testing, or other behaviours.
+
+=cut
+
+package # hide from indexer
+ Future::_Exception;
+
+use overload
+ '""' => "message",
+ fallback => 1;
+
+sub new { my $class = shift; bless [ @_ ], $class; }
+
+sub message { shift->[0] }
+sub category { shift->[1] }
+sub details { my $self = shift; @{$self}[2..$#$self] }
+
=head1 EXAMPLES
The following examples all demonstrate possible uses of a C<Future>
=== added file 't/23exception.t'
--- t/23exception.t 1970-01-01 00:00:00 +0000
+++ t/23exception.t 2019-04-30 15:25:31 +0000
@@ -0,0 +1,49 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Fatal;
+
+use Future;
+
+# ->get throws an object
+{
+ my $f = Future->fail( "message\n", category => qw( a b ) );
+ my $e = exception { $f->get };
+
+ # TODO: some sort of predicate test function to check this
+ is( $e->message, "message\n", '$e->message from exceptional get' );
+ is( $e->category, "category", '$e->category from exceptional get' );
+ is_deeply( [ $e->details ], [qw( a b )], '$e->details from exceptional get' );
+
+ # Still stringifies OK
+ is( "$e", "message\n", '$e stringifies properly' );
+}
+
+# ->fail can accept an exception object
+{
+ # TODO: Some sort of constructor API?
+ my $e = exception {
+ Future->fail( "message\n", category => qw( c d ) )->get;
+ };
+ my $f = Future->fail( $e );
+
+ is_deeply( [ $f->failure ], [ "message\n", category => qw( c d ) ],
+ '->failure from Future->fail on wrapped exception' );
+}
+
+# ->call can rethrow the same
+{
+ my $f1 = Future->fail( "message\n", category => qw( e f ) );
+ my $f2 = Future->call( sub {
+ $f1->get;
+ });
+
+ ok( $f2->is_failed, '$f2 failed' );
+ is_deeply( [ $f2->failure ], [ "message\n", category => qw( e f ) ],
+ '->failure from Future->call on rethrown failure' );
+}
+
+done_testing;