Return error objects instead of throwing exceptions

Programmers generally consider two types of error communication: the “modern” and shiny exception throwing, and the old and decrepit return values. When they consider these, they choose one and forsake the other. One is good, and the other is bad. Programmers won’t agree on which is which though.

The return value technique comes from older languages that had no other convenient way to do it:

my $result = some_function( @args );
if( $result ) {
	...     # handle error
	}
...         # continue with program

This has a problem because the values that you want to return for normal operation get mixed up with those that you want to use to signal the error. You could add some buffer argument to fill in the error, but that’s really annoying, especially since Perl doesn’t have function signatures:

my $value = some_function( \$error, @args );
if( $$error ) {
	...     # handle error
	}
...         # continue with program

To get around this, some languages created a third path for error messages. An exception signals a problem, jumps out of the current code context, and hopes that someone handles it (Item 101: Use die to generate an exception). In Perl, you can use an eval to trap this then inspect the $@ variable for the error (Know the two different forms of eval):

my $value = eval { failing_sub( @args ) };
if( my $error = $@ ) {
	...
	}
...         # continue with program

Actually handling an error from an eval is tricky, so some people recommend using something like Try::Tiny, even though behind its interface its doing essentially the same thing:

use Try::Tiny;

try { failing_sub( @args ) }
	catch { ... }    # handle error
	finally { ... }; #fail over

This still isn’t much better, even if it does its best to handle the trickiness of $@. When you compare it to the other examples you’ve seen so far in this Item, you can’t really tell the difference at the syntax level. You call something, then add code to check a value. Exceptions, as an ideal, might have merit. If your language started with them as a core concept (which Perl did not), you probably have the flexibility you need to use them effectively. A language with exception handlers can both handle the error and pick up at the point of the error. In Perl, once you get the exception, you don’t have any way to get back to the spot where you threw the exception. Essentially, you just have a fancy return value. Put a bit more strongly, you have a crude goto that doesn’t even preserve its context.

Most people have failed to consider something Perly instead. The return value and buffer examples are hold-overs from C-like thinking, and the exceptions are object-oriented envy. Or, more correctly, envy for particular implementations of an object-oriented concept.

Since Perl does not have subroutine signatures, you don’t get to declare what you will give to a subroutine or what you get back from a subroutine. You pass it a list, and you get back a list (even if that is one or no items). In a C-like language, you’d return only one kind of thing, which made the return result a problem. You could return a particular type of struct, but that struct has to be the same type for success and failure. You might be able to force that to work, but then every subroutine returns the same struct and you have to translate that into the right values to pass to other routines. Ugh.

Perl doesn’t care what you return, so why not return an error object in case of an error, and anything else otherwise? The fundamental feature of an object is identity—an object knows what it is. Unless you get an error object, everything worked. When you get the result, you could look for objects of the right type:

use Scalar::Util qw(blessed);

my $results = some_function( @args );
if( blessed($result[0]) && $result[0]->isa( 'MyError' ) && $result[0]->is_error } ) {
	...     # handle error
	}
...         # continue with program

It’s easier, though, just to assume it’s an object and call the is_error method. If it’s not an object, you just catch the method call on the non-object with an eval, that you don’t need to trap. This also lets you use any object that has the is_error interface:

my @results = some_function( @args );
if( eval{ $result[0]->is_error } ) {
	...     # handle error
	}
...         # continue with program

That error object could get fancy, too, with a given-when (although with a for, as in Use for() instead of given()):

my @results = some_function( @args );
if( eval{ $result[0]->is_error } ) {
	for ( $result[0]->type ) {
		when( 'output' )       { ... }
		when( 'no_database' )  { ... }
		when( 'bad_request' )  { ... }
		default                { ... }
		}
	}

You haven’t seen anything about the error object though, mostly because it doesn’t matter. Indeed, this particular interface doesn’t matter. You don’t need anything fancy. The error class just carries some data around. It doesn’t do anything with the data and it doesn’t interrupt your flow control:

package Local::MyError {

	sub new {
		my( $class, $type, $message ) = @_;

		bless {
			message => $message,
			type    => $type,
			caller  => [ caller(1) ],
			};
		}

	sub is_error { 1 }
	sub type     { $_[0]->{type} }
	}

When you need to communicate a failure, you return the error object:

sub some_function {
	...;
	open my $fh, '>', $filename or return Local::MyError->new( ... );
	...;
	}

Such a class isn’t limited to this particular technique either, so you can get more use out of it. If you still want to use exceptions, you can use the error object with die:

sub some_function {
	...;
	open my $fh, '>', $filename or die Local::MyError->new( ... );
	...;
	}

my @results = eval { some_function( @args ) };
if( my $error = $@ and eval { $error->is_error } ) {
	for ( $error->type ) {
		when( 'output' )       { ... }
		when( 'no_database' )  { ... }
		when( 'bad_request' )  { ... }
		default                { ... }
		}
	}

This is very similar to the example in the autodie documentation:

eval {
	use autodie;

	open(my $fh, '<', $some_file);

	my @records = <$fh>;

	# Do things with @records...

	close($fh);

};

given ($@) {
	when (undef)   { say "No error";                    }
	when ('open')  { say "Error from open";             }
	when (':io')   { say "Non?open, IO error.";         }
	when (':all')  { say "All other autodie errors."    }
	default        { say "Not an autodie error at all." }
}

Things to remember

  • Exceptions aren’t that different than return values
  • You can’t resume execution after throwing an exception
  • You can return an error object to signal failure

Post to Twitter Post to Delicious Post to Digg Post to Facebook Post to Reddit

Leave a comment

0 Comments.

Leave a Reply

You must be logged in to post a comment.