Skip Menu |

This queue is for tickets about the future CPAN distribution.

Report information
The Basics
Id: 129373
Status: resolved
Priority: 0/
Queue: future

People
Owner: Nobody in particular
Requestors: leonerd-cpan [...] leonerd.org.uk
Cc:
AdminCc:

Bug Information
Severity: (no value)
Broken in: (no value)
Fixed in: 0.40



Subject: Have ->get and Future->fail wrap additional failure values in an exception object
(Via https://rt.cpan.org/Ticket/Display.html?id=129305) async/await syntax is currently not transparent to additional Future->fail values, including the category name. While initially it's tempted to suggest that's an F-AA bug, it would be solved quite neatly if ->get on a failed future would yield an object that wrapped the additional values, which ->fail would recognise and unwrap, thus preserving transparency in the common case. This has additional benefits entirely outside of F-AA. In particular, such code as my $f1 = ... my $f2 = Future->call( sub { $f1->get } ); should transparently capture all the values from a failure. -- Paul Evans
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
Subject: rt129373.patch
=== 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;
From IRC: 23:52 <tm604> LeoNerd: looks like Future::_Exception is missing a couple of key features? specifically ->as_future / ->from_future, e.g. https://metacpan.org/release/Ryu/source/lib/Ryu/Exception.pm#L94 Those seem good -- Paul Evans
Released in 0.40 -- Paul Evans