Exception Handling in Perl

G. Wade Johnson

Houston.pm

Error Handling Strategies

How do we respond to errors in a program?

One of the major differences between a quick hack and production code is the manner in which the code handles errors. There are several strategies for dealing with errors.

Ignoring Errors

Assume everything goes down the happy path.

This approach is used by quick-and-dirty scripts and one-liners. You probably shouldn't use this approach in any code that is used more than once.

As we all know, the happy path is not guaranteed. It can be interrupted by almost anything from a bad environment to malicious users to bad programming.

We'll ignore this approach.

Error Returns

Return special values to signal when code failed to act correctly.

This is the approach most people are familiar with. If a function is successful, it returns one kind of value. Otherwise, it returns some kind of error indicator.

Since this is the most common approach to error handling, we'll use it as a standard to which we'll compare exceptions.

Global Variables

Signal an error by updating a global variable.

We've all seen this with errno (or $!).

The global variables for error codes have a couple of really serious problems. The first is that they are global, and can therefore be changed by code other than what we are currently checking.

Most global variable-based error handling is also hampered by the fact that no one wants to stomp on an error that's already there. If a function succeeds, it usually doesn't reset the global. This means that if you have an error code, you don't know how long it's been an error.

We won't be looking at this approach further.

Signals

Asynchronous signalling from the OS or hardware.

Requires a special handling routine to recover. Not useful in general, but needs to be listed for completeness.

Abort or Exit

In case of error, stop the program.

Not particularly robust. But, on the positive side, you know there have been no errors before any piece of code that gets executed.

Sometimes used when continuing would be more harmful than crashing. This approach (or actually restarting) is pretty common in embedded programs.

Exceptions

An exception is kind of like a non-local return with information attached.

We'll have a lot more to say about these.

Philosophical Point

bIlujDI' yIchegh()Qo'; yIHegh()!

-- Klingon programming proverb

Philosophical Point

bIlujDI' yIchegh()Qo'; yIHegh()!

-- Klingon programming proverb

It is better to die(); than to return() in failure!

(thanks to Paul Fenwick)

I apologize for inflicting this on you. But, I thought it was funny.

Exceptions in Perl

Of course, there is more than one way to do Exceptions in Perl. But, all forms start with the basic die/eval pair.

You've probably used die before to display a message to STDERR before exiting the program. Exiting the program is the normal behavior for an uncaught exception. Perl provides a way to catch the result of a die with a block eval.

The Exception module converts the string passed to die into an exception object. The module also provides syntactic sugar to make exceptions in Perl look more like exceptions in other languages.

The Exception::Class module provides an approach to easily declaring exception objects.

Why use Exceptions?

Exceptions separate error handling logic from the normal logic of the code.

Done correctly, an exception handling strategy consolidates error checking and recovery into well-defined points in the code. Instead of handling errors where they occur (when you may not have enough information to handle it properly), exceptions are passed to a higher level where better recovery is possible.

Anatomy of Exception Code

This list uses more standard terms to describe exception handling. This terminology is more specific than the keywords we use in Perl.

Simple Exception Example (C++)


    try {
        fragile();
    }
    catch( IOException &ex )
    {
        // recover from IOException
    }
    catch( SQLException &ex )
    {
        // recover from SQLException
    }
    catch( ... )
    {
        // recover from all others
    }

I used C++ for this example because it follows the syntax many people will be more familiar with. C++ and Java are very close in this respect.

The try block specifies the code where we expect exceptions to occur. (Shown here by the function named fragile(). Following the try block are a series of catch clauses that each handle a kind of exception. Inside that block you can do whatever recovery you deem reasonable and either drop out of the block or re-throw the exception.

The final catch anything case is not required. So that we may defer the handling of other exceptions to a higher lever routine.

Perl Equivalent


    eval { fragile(); };
    if(ref $@ and my $ex = $@) {
        if($ex->isa('Exception::IO'))
        {
            # recover from Exception::IO
        }
        elsif($ex->isa('Exception::SQL'))
        {
            # recover from Exception::SQL
        }
        else
        {
            # recover from all others
        }
    }

The Perl equivalent is similar in structure, but is missing the specialized keywords and syntax. Several modules on CPAN, provide syntactic structures to make this system look more like people from other languages might expect.

The most unfortunate thing about the Perl syntax is the reliance on the $@ global variable. If you are not careful, you can easily run code that changes this variable, losing the exception. That's why it is very important to save the value of $@ to another variable as soon as you can.

Exception Advantages

  • Can't accidentally ignore them
  • Simpler code paths
  • Multiple error returns are easy
  • Handle errors at a different level than detected
  • Unhandled exceptions stop the program

Fans of exceptions point out that they have lots of advantages.

Exception Disadvantages

  • Extra code to ignore them
  • Exception path is (sort of) hidden
  • Care is needed to make code exception safe
  • Unhanded exceptions stop the program

The critics of exception handling point out that exceptions have many disadvantages.

Perl Exception Disadvantages

  • Odd syntax: die/eval
  • Throwing strings vs. objects
  • The $@ variable

Perl's exception handling mechanism is somewhat different than that in most languages. People with experience with exception handling in other languages sometimes look down on Perl's approach because it's different.

Using die to throw exceptions, Perl makes the simple exit on error strategy compatible with exceptions. If you don't try to catch an exception with an eval block, the die exits the program.

To support the use of die to display a message and exit, it can use a simple string instead of some special exception object. Many people use regular expressions to look at the thrown string to determine the type of exception. Since die takes

Error Return Advantages

  • Easy to understand
  • Control flow is visible
  • Easy to ignore errors you don't care about
  • Unhandled errors do not stop the program

Probably the most common approach to error handling is error returns. Each function defines what it returns in the case of an error and each calling function tests and handles any errors.

Since the error handling is explicit at each call and return, tracing the control flow in the case of errors should be mostly straight-forward. Moreover, if the error is non-fatal, you can just ignore the return value and continue. If any error condition is not handled, the program is not aborted early, unlike exception.

Error Return Disadvantages

  • Error handling flow obscures program flow
  • Easy to ignore errors you don't know about
  • Unhandled errors are invisible

Despite it's ease of use, the error return approach does have disadvantages. The most obvious problem comes from the explicit nature of the error recovery. A program that does a thorough job of recovering from errors can be almost completely swamped in error recovery code.

The less obvious, but more dangerous, problem is the fact that errors can easily be accidentally missed. There is no way to tell the difference between errors you meant to ignore and ones you accidentally ignored.

Exception Safety

How does the code respond to exceptions?

One complaint that many people make about exceptions is that you have to be very careful because you never know what can throw an exception.

For several years, people have tried different strategies for writing exception-safe code. Most of the approaches that people developed were somewhat ad-hoc, until David Abrahams described the levels of exception safety.

Abrahams Guarantees

  1. No guarantee
  2. Basic guarantee
  3. Strong guarantee
  4. No-throw guarantee

These levels do not tell you how to deal with exceptions, they just provide a way to reason about them.

The first level is not usually on the list, but it is a case you need to consider.

The Basic guarantee covers two issues, no resource leaks and all invariants remain. No exception should leave the code in an unstable or illegal state.

The strong guarantee means that the action either completes successfully or the state of the program is the same as if the code had never executed. This is sometimes called a transactional or atomic guarantee.

The strongest guarantee is the no-throw guarantee which promises that no exceptions are thrown and the code completes successfully.

Practical Exception Handling

Theory is great, but how do exceptions perform in reality?

Without some actual code, it's quite hard to see the trade-offs you need to consider when using exceptions. Most importantly, the difference between exceptions and error returns are much smaller for a small piece of code.

In a 20-line program, it really doesn't matter how you deal with errors. As the code gets larger and more complex, exceptions help to deal with the complexity of error handling.

An Example

  • Complicated enough to show trade-offs
  • Simple enough to be easily understood
  • Short enough that I'm willing to write it multiple times

One problem with trying to demonstrate the usefulness of exceptions is that it becomes more apparent in larger programs. This is a lot like demonstrating objects. Object oriented code and exceptions both seem like mostly overhead in a 20 line program.

Unfortunately, we're not going to want to build a 200K line system two different ways to see the difference. The example that follows is the minimum I could come up with that would begin to show the advantages of exceptions. I hope you can imagine how these differences would expand as a program increases in size.

Example Code without Exceptions

This is a mock program without exceptions.

This is a pointer to some code that has been written without exceptions. It's not real code, but it follows patterns you may have seen.

Example Code Done Correctly

This is the same program with all errors actually checked.

Here's the same code with all of the potential errors covered.

Example with Exceptions

This is the same program with exceptions.

Here's the same code re-written with exceptions.

autodie

Wouldn't it be great if Perl builtins threw exceptions?

The autodie module turns functions that use error returns into functions that use exceptions. Once you get used to using exceptions, this can be extremely handy.

Example with autodie

This is the same program with autodie exceptions.

Let's revisit our code sample with autodie enabled.

Observations

  • Reduced error propagation code
  • Potentially better error coverage
  • Error paths are harder to see

From the examples, we can see some of the trade-offs that are involved in using exceptions. We can see that progation of errors happens pretty much automatically, which can reduce the lines of code in intermediate functions.

Depending on the code, it is possible to get better coverage because the low-level code can always throw exceptions, knowing that higher-level code will do the right thing. This means that you are less likely to miss a critical mistake because you didn't check for it.

However, the error paths are harder to see, because they may happen mostly implicitly.

Exception Hierarchies

  • The one, true hierarchy
  • Lots of small hierarchies

When exception objects are used rather than strings, there is an ongoing debate about whether the exceptions should be in one large hierarchy or multiple smaller hierarchies.

The main argument for the one-big hierachy is the catch everything case. Where you want to be able to catch all exceptions with a single base class.

Like most approaches that attempt to stuff everything into one large hierachical structure, this tends to fall down when you end up forcing very different kinds of exceptions into a parent-child relationship.

Other Exception Modules

  • Exception::Class::TCF
  • Class::Throwable
  • Exception::Class::TryCatch

There are various modules out there that add syntactic sugar to make Perl exception handling look more like other languages. Each has its own trade-offs.

There are also a number of modules that simplify the creation of exception object.

Exception Advice

There are good and bad ways to use exceptions.

Like any new technique, people often go overboard when they actually get it. Here are a few Dos and Don'ts to keep in mind when using exceptions (in any language).

Exception Don't

Don't use exceptions for the normal path.

Exceptions are meant to be used for exceptional or error conditions. They should not be triggered in the normal functioning of the code. You don't need to avoid exceptions, but use them as a tool for the purpose they were designed.

Exception Don't

Don't catch exceptions and rethrow them at every level.

Almost every new user of exceptions comes up with the idea of catching the all exceptions in each function (usually to add a little stack info) and rethrow the exception. This results in a large amount of useless monkey-motion that doesn't actually help matters much.

Exception Don't

Don't catch and ignore exceptions (mostly).

Catching an exception and ignoring it should be a relatively rare circumstance. By doing that, you are ignoring a potential problem in the code and hiding that from higher-level routines that might actually be able to handle it.

Exception Don't

Rarely use catch all handlers except at outermost level or propagating.

Catch-all handlers are also easy to misuse. Except at the outer-most level of your code (where it serves to catch anything that may have been missed elsewhere). The best use of the catch-all handler for Perl is to propagate any exceptions you didn't handle to higher levels.

Exception Don't

Don't use exceptions in the place of simple conditionals.

If a method answers a simple question with a boolean, don't replace it with an exception in the false case.

Exception Don't

Don't use exceptions instead of other flow control.

If the code calls for an if or switch-type construct, use them rather than overload exceptions for that purpose.

Exception Do

Use exceptions to deal with errors and unexpected conditions.

Exceptions can do a very good job of consolidating error handling and recovery. Using them for their intended purpose can make your code more maintainable.

Exception Do

Throw (die) at low-level, as soon as the error is detected.

Often the point where you can detect a problem is where you have the least context for dealing with the problem. Don't try to recover from errors low in the code. Throw an exception and let someone else react appropriately for this circumstance.

Exception Do

Catch (eval) only when you can handle an error.

There's no since in catching an exception, if you can't do anything about it. Allow the exception to propagate to a level with the appropriate context to recover from it.

Exception Do

Partially handle an exception with catch and re-throw idiom.

Sometimes you can't completely deal with an exception, but you can perform some cleanup that would be inconvenient at a higher level. In this case, it is perfectly reasonable to do some recovery and then rethrow the exception for another level to finish the recovery.

Exception Do

Catch-all at outer-most level of the code is a good idea.

No matter how careful you have been, eventually an exception is thrown for which you have no recovery code. It may be caused by maintenance changes changes to a library, or just something you missed. A catch everything case at the highest level of the code prevents this from being a disaster.

Exception Do

Converting exceptions at module boundaries can be a good idea.

In many cases, it is a good idea to catch exceptions at the boundaries of (third-party) modules and convert them into exceptions that your code is able to deal with. This is especially important if the exceptions from the module are very low-level. Logging the exception and converting it into a more generic exception may simplify later code.

Exception Do

Use lexical variables and object lifetime for safe resource management.

By using lexical variables and class DESTROY methods, as appropriate, Perl cleans up resources for you automatically in the presence of exceptions.