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 ); `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. 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 . SEE ALSO Result::Trait, . AUTHOR Toby Inkster . 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.