Skip to content

Instantly share code, notes, and snippets.

@Ovid
Last active March 21, 2022 06:51
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Ovid/5205534f7dcc52e4d931aaff301b39aa to your computer and use it in GitHub Desktop.
Save Ovid/5205534f7dcc52e4d931aaff301b39aa to your computer and use it in GitHub Desktop.
Exceptions in Perl?

Preface

This is something that likely cannot be made into an RFC for the Perl language at this time because implementation would be greatly simplified when the Corinna object model is in core. For example, a base class for what is discussed might look like the following:

# Exception is a poor name for warnings, so a better name is warranted
class Exception :version(v0.1.0) {
    # $message and $description might be from a messaging role
    field $message     :reader :param;
    field $description :reader :param { "" };
    
    # must not be lazy because stack frames might not be available later
    field $stack_trace :reader { $self->build_stack_trace };
    
    method build_stack_trace :private () {
        # generate stacktrace
    }
    
    method throw () {
        die $self->to_string;
    }
    
    method to_string () {
        return join "\n", $message, $description, $stack_trace;
    }
}

Exceptions

Here's a common problem in Perl:

try {
    $object->some_method;
}
catch ($error) {
    if ( $error =~ /File doesn't exist/ ) {
        ...
    }
    else {
        croak($error);
    }
}

And some programmer sees the File doesn't exist string in the code and changes to the more standard File not found, but misses error handling that's based on string matching.

But exceptions make this safer:

try {
    $object->some_method;
}
catch ($exception) {
    if ( $exception isa Exception::FileNotFound ) {
        ...
    }
    else {
        $exception->throw;
    }
}

With that, you're testing the exception class, something far safer. In fact, at this point, you can localize exception strings. In French, you might get « Fichier introuvable » which, for a French speaker, might be much easier to understand. This wouldn't change the class of the exception, so it's more maintainable.

Exception objects, of course, could have a message, a more verbose description, a stacktrace, and so on.

Exception Hierarchy

What would an exception hierarchy look like in Perl, though? Would it be a tree or a graph (inheritance versus roles). I'm assuming Perl wouldn't handle "checked" exceptions (exceptions that perl requires be trapped at compilation time), but there would still need to be some organization.

For example, in Java, we have both Error and Exception, separate related classes, each of which inherits from Throwable. An Error is thrown when there's an unrecoverable error in a program, such as an AbtractMethodError where a subclass has failed to override an abstract method in a parent class. Even though code might trap an Error (for logging or sending email, for example), it should still probably rethrow that error.

An Exception, on the other hand, is an error that might be recoverable. A FileNotFound exception might cause the program halt, or it might prompt the user to manually enter values that should be in a config file.

However, we can't blindly port over an exception system from other languages. This is Perl, after all. Just reading through the Java error/exception hierarchy makes it clear that much of it doesn't apply to a dynamic language.

But what about this case? (A common issue with tests)

my $warning = capture_stderr {
    $object->some_method;
};
like $warning, qr/$some_warning/, 'We have our expected warning';

And someone changes the warning message and tests fail (guess what I've had to work on yesterday?) What if warnings were objects?

my $warning = capture_stderr {
    $object->some_method;
};
ok $warning isa Warning::Category, 'We have our expected warning';

So actually, we might want a generic messaging mechanism (localizable, I should think) that can be broadly applicable to many problem spaces, or we might want warning objects which are not throwable, though you could probably promote them to an exception and throw them.

And that got me thinking.

Exception Levels

If you use logging software in Perl, you often find "log levels". In the excellent Log::Log4Perl module, here are the log levels:

$logger->trace("...");  # Log a trace message
$logger->debug("...");  # Log a debug message
$logger->info("...");   # Log a info message
$logger->warn("...");   # Log a warn message
$logger->error("...");  # Log a error message
$logger->fatal("...");  # Log a fatal message

I couldn't help but think that "exception levels" might be a better way to think about an exception hierarchy (at which point, the word "exception" doesn't seem appropriate), but now things get interesting. Imagine this:

use exceptions 'warn';

sub first () {
    info "This is a no-op due to the exception level";
}

sub second () {
    warn "This is trappable, but untrapped, prints this message";
}

sub third () {
    error "This is trappable, but untrapped, prints this message, a stacktrace, and dies";

sub fourth () {
    fatal "This is probably trappable (?), but untrapped, prints this message, a stacktrace, and dies";
}

No, I am not suggesting the above syntax (too many clashes with existing Perl). Further, I can see pitfalls with this (easy to swallow warnings), but I wanted to start a discussion about a conceptual framework of different types of informational messages that the program might emit.

All of the above might consume a "Message" role to make it easier to localize messages for various spoken languages. Said role might even be more generally useful outside of exceptions, though this might be unworkable due to the need to maintain translation files. Careful thought might be needed here.

Corinna

Many things for the above would be easier to implement when the Corinna object system gets into the Perl core (work on this is expected to begin after the next major release).

Conclusion

Currently, Perl has no native way of generating exception objects and the CPAN has tons of competing different implementations. Proper exceptions can offer a clean standard, make code more robust, but the dynamic nature of Perl suggests to me that we might want to think about exceptions in a new way.

@HaraldJoerg
Copy link

I totally like using objects for exceptions. Actually, this has been around for some time: die can pass an object, and a surrounding eval block can pick it up and use the object's properties. So what is new in this proposal? Should Perl core provide a common base class for exception objects? I actually doubt that there's much value in that, given how different Perl applications are.

In a rather large (Java) application I've been working on, we could collect a lot of experience with exceptions and it doesn't fit well with the example class at the beginning of this gist. Here are some things I noticed:

  • The method build_stack_trace is private? This means it is unconditionally called and can't be overridden by a subclass? Bad idea. In my experience stack traces are rarely useful. What you often would like to know are the actual parameters used in a call. This makes some sense in a language like C, but less so in Perl. If parameters are objects, then you can't inspect them (Dios), or at least not easily (Object::Pad). And if you can, you get tons of context, mostly useless. It took developers some time to realize that users of a web application don't appreciate looking at stack traces in their browser.
  • message is mandatory? If so, then the message string is part of the API and must not be changed. The text should be machine-readable, otherwise localization with e.g. Locale::Maketext won't work. If you change the text, then tests should break, to remind you about the consequences.
  • Java can't do that, but Perl can, and should: to_string should be overloaded for stringification. If an exception isn't caught where you expect it to be caught, this is an easy safeguard against the dreaded died: Exception(0x55b3956e92e8) in logfiles.

Some random remarks:

  • About Exceptions should be used for exceptions, not flow control. - It depends. If a condition can supply useful information about what went wrong, this can be transported in an exception object, without any need for a stack trace.
  • About Something like an 'autoexception' mechanism could be very useful - Perl has autodie. And guess what: It actually returns exception objects. So, you can distinguish between "file not found" and "permission denied" without parsing $! strings which depend on platform and language by looking at the numerical errno.
  • About FileException and their likes: This is, at best case, a category at the lowest level. I suggest that such exceptions are caught early, and the application should then provide the appropriate context. open doesn't know whether a failure is a user error or a bug in the software, but the application does. Localization of messages is relevant for user errors - but not for software bugs, and stack traces are relevant for software bugs - but not for user errors.

Ah, and because I've actually seen this one in the wild:

try {
    open my $fh, '<', $filename;
}

Subsequent reads will read from whatever $fh was in scope before the try block. This is fun to figure out if there actually is such a variable!

@duncand
Copy link

duncand commented Mar 4, 2022

@HaraldJoerg said:

  • It took developers some time to realize that users of a web application don't appreciate looking at stack traces in their browser.

Yes, but its much bigger than that. Lazy web app developers who simply print out the internal exception or error details on a web page are opening themselves up for huge security/privacy/experience issues. You should NEVER (except in private testing environments) be displaying any details of your inner workings to web users. Instead they should be getting a nondescript message that something was wrong, and the details only logged internally. The only times users should be getting details is when the problem is the user's fault, and then it should only be specifically relevant to what they did wrong, not how the service internally failed. So whether exceptions provide stack traces or anything else have nothing to do with browsers.

@Ovid
Copy link
Author

Ovid commented Mar 4, 2022

@HaraldJoerg @duncand Thank you for the great feedback!

@merrilymeredith
Copy link

I'd really appreciate exception objects in core, and core exceptions being objects even more. I think, much like the object system itself, there's an advantage to having a few key interfaces blessed to be part of core rather than let the library ecosystem have several different takes on it. And just as you mentioned the possibility of localized messages, getting this first step in opens the door to applications customizing to their needs just by changing the top of the hierarchy, applying behaviors through Roles, or what-have-you. To do it today with core string exceptions means you have fiddle with things when they're caught every time.

For more hierarchy inspo from a dynamic language rather than static, you might look to Ruby.

Something I'd keep in mind is an interface for an exception that wraps another - forr creating a wrapper, reading the wrapped value, and the wrapper's behavior with regard to error type matching. That's useful to contextualize errors beyond just what the stack trace says, such as a higher-level error (say, CreateBackupError) that wraps the low-level cause (NoSpaceError). It's maybe not super important but I see it as another case where it's good to have a blessed interface: so you don't have several styles of the same behavior where the accessor is named "cause" vs "wrapped" vs "inner" and everyone has to be defensive about which they happen to be catching at the time. Ruby has "cause" and it actually tries to capture the cause for you, while Go (≥ 1.13) uses "Unwrap()" and an inner error must be wrapped explicitly.

@daotoad
Copy link

daotoad commented Mar 9, 2022

Thanks for putting this together. Exception handling has been in need of some TLC for quite a while.

One of my major peeves with most logging systems is the notion that log levels should be an ordered set, with level N implying that all levels 1 through N are enabled. Treating log levels as mutually independent gives much more control when adjusting the verbosity of instrumentation.

Is trace greater or less than debug? Will you always want to log errors when you want to log warnings?

A few years ago I worked on a large code base that was in dire need of instrumentation to address all manner of Heisenbugs and other undesirable behavior. We were able to bring things under control by using a logging system that allowed us to toggle 7 different log level bits over lexical scopes. This strategy worked very well and helped us focus our testing efforts improved our system monitoring.

To me it feels like there are a lot of unquestioned assumptions built into how we all think about logging that we would do well to root out.

Baking in a hard dependency on log levels as being inherently ordered would enforce a particular, restrictive set of capabilities on anything that uses it.

@duncand
Copy link

duncand commented Mar 9, 2022

I completely agree with @daotoad . And that made me think of a great idea, something there is a LOT of precedent for already. Don't think of logging as levels, instead think that the system provides a set of named channels, some code sends messages to those channels by name, other code reads messages from those channels by name. Have distinct channels for whatever level of granularity of subject matter one might want to log about. So for example, channels named info and error and warning and whatever. Perl defines some channel names that it uses to log things to, users can choose to log to the same channels or new ones that they declare. Loggers are set to read from whatever channels the user wants and output it to files/console/etc. In the general case, messages are objects. See the Postgres feature NOTIFY for example https://www.postgresql.org/docs/13/sql-notify.html of precedent.

@duncand
Copy link

duncand commented Mar 9, 2022

Addendum, or similar to with exception objects, you could choose to have one notification channel, and messages are objects of some hiararchy, like exceptions have a hierarchy. Rather than separate named channels, there's one channel and readers can just filter on what message classes they are interested in, either individually or on any point in the hierarchy to include subtypes etc.

@duncand
Copy link

duncand commented Mar 9, 2022

Further to the last 2 points, Perl could be configured that whenever it throws an exception object it ALSO posts the same exception object to the notification queue. So exceptions and notifications are separate concepts but can easily be used together.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment