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.
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.
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.
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.
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.
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.
An exception is kind of like a non-local return with information attached.
We'll have a lot more to say about these.
bIlujDI' yIchegh()Qo'; yIHegh()!
-- Klingon programming proverb
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.
die
/eval
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.
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.
try
blockcatch
blockthrow
exceptionfinally
blockThis list uses more standard terms to describe exception handling. This terminology is more specific than the keywords we use in Perl.
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.
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.
Fans of exceptions point out that they have lots of advantages.
The critics of exception handling point out that exceptions have many disadvantages.
die
/eval
$@
variablePerl'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
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.
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.
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.
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.
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.
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.
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.
This is the same program with all errors actually checked.
Here's the same code with all of the potential errors covered.
This is the same program with exceptions.
Here's the same code re-written with exceptions.
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.
autodie
This is the same
program with autodie
exceptions.
Let's revisit our code sample with autodie
enabled.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.