What will we cover?
Error handling is the process of catching the errors generated by our program and hiding them from our users. It doesn't matter too much if we, as programmers get exposed to a Python error message - we are supposed to understand all that techno speak. But our users are probably not programmers and they want nice, easy to understand messages when things go wrong, and ideally, they want us to catch the error and fix it without them ever knowing about it!
And that's where error handling comes in. Almost every language provides a mechanism for catching errors as they occur, finding out what went wrong and, if possible, taking appropriate action to fix the problem. Over time there have been a number of different approaches adopted to do this and we tackle the subject by following the historical development of the technology. In that way you can hopefully appreciate why the new methods have been introduced. At the end of the topic you should be able to write user friendly programs that never allow a hint of a Python error message to be seen by your users.
In traditional BASIC, programs were written with line numbers to mark each one of code. Transferring control was done by jumping to a specific line using a statement called GOTO (we saw an example of this in the Branching topic). Essentially this was the only form of control possible. In this environment a common mode of error handling was to declare an errorcode variable that would store an integer value. Whenever an error occurred in the program the errorcode variable would be set to reflect the problem - couldn't open a file, type mismatch, operator overflow etc
This led to code that looked like this fragment out of a fictitious program:
1010 LET DATA = INPUT FILE 1020 CALL DATA_PROCESSING_FUNCTION 1030 IF NOT ERRORCODE = 0 GOTO 5000 1040 CALL ANOTHER_FUNCTION 1050 IF NOT ERRORCODE = 0 GOTO 5000 1060 REM CONTINUE PROCESSING LIKE THIS ... 5000 IF ERRORCODE = 1 GOTO 5100 5010 IF ERRORCODE = 2 GOTO 5200 5020 REM MORE IF STATEMENTS ... 5100 REM HANDLE ERROR CODE 1 HERE ... 5200 REM HANDLE ERROR CODE 2 HERE
As you can see almost half of the main program is concerned with detecting whether an error occurred. Over time a slightly more elegant mechanism was introduced whereby the detection of errors and their handling was partially taken over by the language interpreter, this looked like:
1010 LET DATA = INPUTFILE 1020 ON ERROR GOTO 5000 1030 CALL DATA_PROCESSING_FUNCTION 1040 CALL ANOTHER_FUNCTION ... 5000 IF ERRORCODE = 1 GOTO 5100 5010 IF ERRORCODE = 2 GOTO 5200
This allowed a single line to indicate where the error handling code would reside. It still required the functions which detected the error to set the ERRORCODE value but it made writing (and reading!) code much easier.
So how does this affect us? Quite simply Visual Basic still provides this form of error handling although the line numbers have been replaced with more human friendly labels. VBScript as a descendant of Visual Basic provides a severely cut down version of this. In effect VBScript allows us to choose between handling the errors locally or ignoring errors completely.
To ignore errors we use the following code:
On Error Goto 0 ' 0 implies go nowhere SomeFunction() SomeOtherFunction() ....
To handle errors locally we use:
On Error Resume Next SomeFunction() If Err.Number = 42 Then ' handle the error here SomeOtherFunction() ...
This seems slightly back to front but in fact simply reflects the historical process as described above.
The default behavior is for the interpreter to generate a message to the user and stop execution of the program when an error is detected. This is what happens with GoTo 0 error handling, so in effect GoTo 0 is a way of turning off local control and allowing the interpreter to function as usual.
Resume Next error handling allows us to either pretend the error never happened, or to check the Error object (called Err) and in particular the number attribute (exactly like the early errorcode technique). The Err object also has a few other bits of information that might help us to deal with the situation in a less catastrophic manner than simply stopping the program. For example we can find out the source of the error, in terms of an object or function etc. We can also get a textual description that we could use to populate an informational message to the user, or write a note in a log file. Finally we can change error type by using the Raise method of the Err object. We can also use Raise to generate our own errors from within our own Functions.
As an example of using VBScript error handling lets look at the common case of trying to divide by zero:
<script type="text/vbscript"> Dim x,y,Result x = Cint(InputBox("Enter the number to be divided")) y = CINt(InputBox("Enter the number to divide by")) On Error Resume Next Result = x/y If Err.Number = 11 Then ' Divide by zero Result = Null End If On Error GoTo 0 ' turn error handling off again If VarType(Result) = vbNull Then MsgBox "ERROR: Could not perform operation" Else MsgBox CStr(x) & " divided by " & CStr(y) & " is " & CStr(Result) End If </script>
In recent programming environments an alternative way of dealing with errors known as exception handling works by having functions throw or raise an exception. The system then forces a jump out of the current block of code to the nearest exception handling block. The system provides a default handler which catches all exceptions which have not already been handled elsewhere and usually prints an error message then exits. (Refer back to the Getting Started topic for a reminder about how to read and interpret Python error messages.)
One big advantage of this style of error handling is that the main function of the program is much easier to see because it is not mixed up with the error handling code, you can simply read through the main block without having to look at the error code at all.
Let's see how this style of programming works in practice.
The exception handling block is coded rather like an if...then...else block:
try: # program logic goes here except ExceptionType: # exception processing for named exception goes here except AnotherType: # exception processing for a different exception goes here else: # here we tidy up if NO exceptions are raised
Python attempts to execute the statements between the try and the first except statement. If it encounters an error it will stop execution of the try block and jump down to the except statements. It will progress down the except statements until it finds one which matches the error (or exception) type and if it finds a match it will execute the code in the block immediately following that exception. If no matching except statement is found, the error is propagated up to the next level of the program until, either a match is found or the top level Python interpreter catches the error, displays an error message and stops program execution - this is what we have seen happening in our programs so far.
If no errors are found in the try block then the final else block is executed although, in practice, this feature is rarely used. Note that an except statement with no specific error type will catch all error types not already handled. In general this is a bad idea, with the exception of the top level of your program where you may want to avoid presenting Python's fairly technical error messages to your users, you can use a general except statement to catch any uncaught errors and display a friendly "shutting down" type message. (You should probably log the error data in a log file though, for future analysis.)
It is worth noting that Python provides a traceback module which enables you to extract various bits of information about the source of an error, and this can be useful for creating log files and the like. I won't cover the traceback module here but, if you need it, the standard module documentation provides a full list of the available features.
Let's look at a real example now, just to see how this works:
value = input("Type a divisor: ") try: value = int(value) print( "42 / %d = %d" % (value, 42/value) ) except ValueError: print( "I can't convert the value to an integer" ) except ZeroDivisionError: print( "Your value should not be zero" ) except: print( "Something unexpected happened" ) else: print( "Program completed successfully" )
If you run that and enter a non-number, a string say, at the prompt, you will get the ValueError message, if you enter 0 you will get the ZeroDivisionError message, if you hit Ctrl-C it will raise a KeyboardInterrupt exception and you'll see the "Something unexpected..." message and, finally, if you enter a valid number you will get the result plus the "Program completed" message.
There is another type of 'exception' block which allows us to tidy up after an error, it's called a try...finally block and typically is used for closing files and network or database connections etc. The finally block is always executed last regardless of what happens in the try section.
try: # normal program logic finally: # here we tidy up regardless of the # success/failure of the try block
This becomes very powerful when combined with a try/except block. It looks like this:
print( "Program starting" ) try: data = open("data.dat") print( "data file opened" ) value = int(data.readline().split()) print( "The calculated value is %s" % (value/(42-value)) ) except ZeroDivisionError: print( "Value read was 42" ) finally: data.close() print( "data file closed" ) print( "Program completed" )
Note: The data file should contain a line with a number in the 3rd field, something like:
Foo bar 42
In this case the data file will always be closed regardless of whether an exception is raised in the try/except block or not. Note that this is different behavior to the else clause of try/except because it only gets called if no exception is raised, and equally simply putting the code outside the try/except block would mean the file was not closed if the exception was anything other than a ZeroDivisionError. Only by adding a finally block can we ensure that the file is always closed.
Also notice that I put the open() statement inside the try/except block. If I'd actually wanted to catch a file open error all I'd need to do is add another except block for an IOError. Why not try that yourself then try opening a non-existent file to see it in action?
What happens when we want to generate exceptions for other people to catch, in a module say? In that case we use the raise keyword in Python:
numerator = 42 denominator = int( input("What value will I divide 42 by?") ) if denominator == 0: raise ZeroDivisionError
This raises a ZeroDivisionError exception which can be caught by a try/except block. To the rest of the program it looks exactly as if Python had generated the error internally.
Another use of the raise keyword is to propagate an error to a higher level in the program from within an except block. For example we may want to take some local action, log the error in a file say, but then allow the higher level program to decide what ultimate action to take. It looks like this:
def div127by(datum): try: return 127/(42-datum) except ZeroDivisionError: logfile = open("errorlog.txt","a") logfile.write("datum was 42\n") logfile.close() raise try: div127by(42) except ZeroDivisionError: print( "You can't divide by zero, try another value" )
Notice how the function div127by() catches the error, logs a message in the error file and then passes the exception back up for the outer try/except block to deal with by calling raise with no specified error object.
Let's combine those two fragments into a single program that illustrates error handling at work:
def div127by(datum): try: return 127/(42-datum) except ZeroDivisionError: logfile = open("errorlog.txt","a") logfile.write("datum was 42\n") logfile.close() raise try: divisor = int( input("What value will I divide by?") ) if divisor == 0: raise ZeroDivisionError print( "The result is: ", div127by(divisor) ) except ZeroDivisionError: print( "You can't divide by zero, try another value" )
So if the user enters 42 or 0 then we will produce a ZeroDivisionError (even though 0 is actually a safe value in this case!), otherwise we print the result of the division and log the input value in the file errorlog.txt.
Python provides a wide range of standard error types and wherever possible we should reuse those standard error types. However, occasionally, we might not find one that really fits our scenario. Fear not, there is a solution.
We can also define our own exception types for even finer grained control of our programs. We do this by defining a new exception class (we briefly looked at defining classes in the Raw Materials topic and will look at it in more detail in the Object Oriented Programming topic later in the tutorial). Usually an exception class is trivial and contains no content of its own, we simply define it as a sub-class of Exception and use it as a kind of "smart label" that can be detected by except statements. A short example will suffice here:
>>> class BrokenError(Exception): pass ... >>> try: ... raise BrokenError ... except BrokenError: ... print( "We found a Broken Error" ) ...
Note that we use a naming convention of adding "Error" to the end of the class name and that we inherit the behavior of the generic Exception class by including it in parentheses after the name - we'll learn all about inheritance in the OOP topic.
One final point to note on raising errors. Up until now we have quit our programs by importing sys and calling the exit() function. Another method that achieves exactly the same result is to raise the SystemExit error, like this:
>>> raise SystemExit
The main advantage being that we don't need to import sys first.
Catching errors is done by using a try block with a set of catch statements, almost identically to Python:
One big difference is that you only get to use one catch statement per try construct, you have to examine the error passed to see what kind it is inside the catch block. This is, in my view, a bit more messy than Python's multiple except style based on exception type. You can see a basic example of testing the error value in the code below.
And that's all I'll say about error handling. As we go through the more advanced topics coming up you will see error handling in use, just as you will see the other basic concepts such as sequences, loops and branches. In essence you now have all of the tools at your disposal that you need to create powerful programs. It might be a good idea to take some time out to try creating some programs of your own, just a couple, to try to sound these ideas into your head before we move on to the next set of topics. Here are a few sample ideas:
To complete any of the above you will need to use all of the language features we have discussed and probably a few of the language modules too. Remember to keep checking the documentation, there will probably be quite a few tools that will make the job easier if you look for them. Also don't forget the power of the Python >>> prompt. Try things out there until you understand how they work then transfer that knowledge into your program - it's how the professionals do it! Most of all, have fun!
See you in the Advanced section :-)