The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

results - why throw exceptions when you can return them?

SYNOPSIS

use results;

sub to_uppercase {
  my $str = shift;
  
  return err( "Cannot uppercase a reference." ) if ref $str;
  return err( "Cannot uppercase undef." ) if not defined $str;
  
  return ok( uc $str );
}

my $got = to_uppercase( "hello world" )->unwrap();

DESCRIPTION

This module is a Perl implementation of Rust's standard error handling mechanism. Rust doesn't have a try/catch/throw mechanism for throwing errors. Instead, functions can be declared as returning a "Result" which may be an "Ok" result or an "Err" result. Callers of these functions will get a compile-time error if they do not inspect the result and potentially deal with the error. (There is syntactic sugar for propagating the error further up the call stack.)

Recent versions of Perl provide try/catch/throw (though throw is spelled "die"), and in older versions the same thing can be roughly accomplished using eval or CPAN modules, making Rust's error handling seem fairly foreign. For this reason I do not recommend using the a mixture of try/catch/throw error handling and Result-based error handling in the same codebase. Pick one or the other.

Result-based error handling can provide some pretty succinct idioms, so I do think it is worthy of consideration.

RETURNING RESULTS

Introduction

If you decide that your function should return a Result object, your function should always return a Result object.

Do not return Results for errors but bare values for success. The following example is bad because the caller cannot rely on the result of to_uppercase always being a Result object.

use results;

sub to_uppercase {
  my $str = shift;
  
  return err( "Cannot uppercase a reference." ) if ref $str;
  return err( "Cannot uppercase undef." ) if not defined $str;
  
  return uc $str;  # BAD
}

Instead:

use results;

sub to_uppercase {
  my $str = shift;
  
  return err( "Cannot uppercase a reference." ) if ref $str;
  return err( "Cannot uppercase undef." ) if not defined $str;
  
  return ok( uc $str );  # FIXED
}

die can still be used in code that uses result-based error handling, but it should only be used for errors that are thought to be unrecoverable. Don't expect your caller to catch exceptions.

Functions

The results module provides three functions used to return results. These functions should nearly always be prefixed with Perl's return keyword.

ok()

The ok() function returns a successful result. It can be called without arguments to represent success without any particular value to return.

return ok();    # success

You may also include a value:

sub your_function () {
  ...;
  return ok( $output );
}

Or multiple values:

sub your_other_function () {
  ...;
  return ok( $count, \@output );
}

The caller can then retrieve those values using:

my $output = your_function()->unwrap();
my ( $count, $output_ref ) = your_other_function()->unwrap();

If a list of return values was provided, then calling unwrap() in scalar context will return the last item on the list only.

ok_list()

This function acts identically to ok() except that calling unwrap() in scalar context will die.

Your caller should never need to check at runtime whether it got an ok() result or an ok_list() result. For any given function, you should settle on just one of them. ok() is usually the best choice as it can still be used in list context.

ok_list() is not exported by default, but can be requested:

use results qw( :default ok_list );

Note that wantarray will be useless in your function because the caller will always be expecting a single scalar Result object. (Which may or may not contain a list of values!)

err()

The err() function returns an error, or unsuccessful result.

It can be called without arguments to represent a general sense of doom, but this is usually a bad idea:

return err();    # failed

It is generally better to give a reason why your function failed:

return err( "This feature isn't implemented" );

Or even better, an exception object:

return err( MyApp::Error::NotImplemented->new );

The results::exceptions module provides a very convenient way to create a large number of lightweight exception classes suitable for that.

Like ok() this can take a list:

return err( MyApp::Error::Net->new, 0 .. 99 );

This would be unusual though, and is not generally recommended.

The :Result Attribute

You can declare that your function always returns a Result using an attribute.

sub to_uppercase : Result {
  ...;
}

If you have Type::Utils installed, then you can even specify the "inner" type for successful Results, though this assumes that your Results are scalars.

sub to_uppercase : Result(Str) {
  ...;
}

This declaration is only checked if one of the PERL_STRICT, AUTHOR_TESTING, RELEASE_TESTING, or EXTENDED_TESTING environment variables is set to true. Otherwise, the attribute operates on the "honour system"!

Exception Objects

It is often easier for your caller to deal with exception objects rather than string error messages. This module comes with results::exceptions to make creating these a little easier. The example in the "SYNOPSIS" section could be written as:

use results;
use results::exceptions qw( UnexpectedRef UnexpectedUndef );

sub to_uppercase {
  my $str = shift;
   
  return UnexpectedRef->err if ref $str;
  return UnexpectedUndef->err if not defined $str;
  
  return ok( uc $str );
}

HANDLING RESULTS

Introduction

If you call a function which returns a Result, you are required to handle the result in some way. If a Result goes out of scope or otherwise gets destroyed before being handled, this is considered a programming error. Currently this will only result in a warning being printed, as Perl demotes exceptions thrown in destructors to warnings.

Results are blessed objects and should be handled by calling methods on them.

Function

is_result( $val )

Returns true if $val is a Result object. You should rarely need to use is_result() because a function which returns Results should never return anything that isn't a Result.

is_result() is not exported by default, but can be requested:

use results qw( :default is_result );

Methods

The full set of methods available on Results is documented in Result::Trait, but a few important ones are described here. These methods are is_err(), is_ok(), unwrap(), unwrap_err(), expect(), and match().

$result->is_err()

Returns true if and only if the Result is an error.

$result->is_ok()

Returns true if and only if the Result is a success.

$result->unwrap()

Called on a successful Result, returns the result.

May be called in scalar or list context, and may return a list if ok() was given a list.

If called on an unsuccessful result (error), will promote the error to a fatal error. (That is, calls die.)

my $upper_name = to_uppercase( $name );

if ( $upper_name->is_ok() ) {
  say "HELLO ", $upper_name->unwrap();
}
else {
  warn "An error occurred!";
}

If unwrap is called, the Result is considered to be handled.

$result->unwrap_err()

Called on a unsuccessful Result, returns the error.

May be called in scalar or list context, and may return a list if err() was given a list. A list of multiple values rarely makes sense though.

If called on a successful result (error), will result in a fatal error. (That is, calls die.)

my $upper_name = to_uppercase( $name );

if ( $upper_name->is_ok() ) {
  say "HELLO ", $upper_name->unwrap();
}
else {
  warn "An error occurred: " . $upper_name->unwrap_err();
}

If unwrap_err is called, the Result is considered to be handled.

$result->expect( $msg )

Similar to unwrap, but if called on an unsuccessful Result, dies with the given error message.

If expect is called, the Result is considered to be handled.

$result->match( %dispatch_table )

This provides an easy way to deal with different kinds of Results at the same time.

$result->match(
  err_Unauthorized   => sub { ... },
  err_FileNotFound   => sub { ... },
  err                => sub { ... },  # all other errors
  ok                 => sub { ... },
);

Other methods

See Result::Trait for other ways to concisely handle Results.

DIFFERENCES WITH RUST

Rust is strongly typed and can check many things at compile time which this implementation cannot. These must all be done through self-discipline in Perl. This includes:

  • Ensuring that functions which return a Result cannot return a non-Result.

  • Ensuring that the recipient of a Result handles that Result.

  • Ensuring that the type of the value inside the Result is expected by the recipient. (Result::Trait includes a handful of methods for run-time enforcement of type constraints though.)

Methods related to Rust's borrowing, copying, and cloning are not implemented in Result::Trait as they do not make a lot of sense.

EXPORTS

This module exports four functions:

  • err

  • ok

  • ok_list

  • is_result

By default, only the first two are exported, but you can list the functions you want like this:

use results qw( err ok ok_list is_result );

Or just:

use results -all;

You can import no functions using:

use results ();

And then just refer to them by their full name like results::ok().

You can rename functions:

use results (
  ok   => { -as => 'Okay' },
  err  => { -as => 'Error' },
);

Renaming imports may be useful if you find the default names conflict with other modules you're using. In particular, Test::More and other Perl testing modules export a function called ok.

Lexical exports

If you have Perl 5.37.2 or above, or install Lexical::Sub on older versions of Perl, you can import this module lexically using:

use results -lexical;

# or
use results -lexical, -all;

# or
use results -lexical, (
  ok   => { -as => 'Okay' },
  err  => { -as => 'Error' },
);

results::exceptions also supports lexical exports:

use results::exceptions -lexical, qw(
  UnexpectedRef
  UnexpectedUndef
);

BUGS

Please report any bugs to https://github.com/tobyink/p5-results/issues.

SEE ALSO

Result::Trait, https://doc.rust-lang.org/std/result/.

AUTHOR

Toby Inkster <tobyink@cpan.org>.

COPYRIGHT AND LICENCE

This software is copyright (c) 2022 by Toby Inkster.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.

DISCLAIMER OF WARRANTIES

THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.