CODE WITH MARTIN

15. Error Handling


Introduction

As a developer, one of the best skills you can develop is knowing how to handle programming errors. When we write code, things are not always going to work for various reasons and at each step in your source code, we have to ask ourselves "What if my program fails to do this step?".

Up until now, if the code that we've written produces an error, our entire application crashes. Even worse, when it does crash, the error message produced is a Python error which can look kinda scary to new programmers and leave us confused and frustrated. But there are ways with Python and most other languages, to handle errors more gracefully.

If we were writing a program like a web-site, we do not want the entire web-site to crash when someone encounters an error in our code. The language feature in Python that helps us with error handling is a try/except block.

Try Except Blocks

Let's write some code that will purposely fail and then look at how we make this error proof. Consider the following code that is trying to convert a string variable in to an integer type.

text = "1234A"

converted = int(text)

print("The converted number is", converted)

Run the code and you will see the following in the terminal output:

Traceback (most recent call last):
  File "c:\Users\Martin\Documents\Code\convert.py", line 3, in 
    converted = int(text)
ValueError: invalid literal for int() with base 10: '1234A'

Our code on line 3 is trying to take the text from the string variable 'text' and convert it to an integer variable using the 'int' function. Because the text in the string variable 'text' on line 1 contains the character 'A', Python fails to perform the conversion and so our program crashes with the ugly error message we see in the terminal.

Our program didn't even make it to line 5 where we try to print the converted number. It simply crashed and ended on line 3.

Now let's add a try except block around the code to handle the error more gracefully. Here's the code:

text = "1234A"

try:
    converted = int(text)

    print("The converted number is", converted)
except:
    print("Failed to convert the text to a number :(")

print("The program has now ended.")

Running the code, you now get the following terminal output:

Failed to convert the text to a number :(
The program has now ended.

Great, it looks like our code ran to the end and managed to print the message on line 10, even though our problem with converting the text is still present. So let's look at what's happening now.

Line 3 is our new keyword 'try' that tells Python that all the code indented under this line (just like how an if statement indents) will watch out for errors. If any errors occur, Python will jump to the 'except' line on line 7. All lines indented under the 'except' line will now be executed.

Our program is still blowing up on line 4, but we see the message from line 8 because that's inside the 'except' block, but most importantly, after the indented lines under 'except', the program continues. So Python continues and executes our line of code on line 10 too, great, we gracefully handled the error and our program didn't die!

Handling Specific Errors

When errors are produced in a large block of code, we may want to know when different types of errors happened. Python usually gives us a specific reason why the error happened. It communicates this to us through the use of an object that was created from a class.

We can check for the different error object types that may occur. Consider the following code:

text = "1234A"

try:
    converted = int(text)

    print("The converted number is", converted)
except ValueError:
    print("There is an error with the value in the string.")
except:
    print("An unknown error occurred.")

print("The program has now ended.")

Running this will now give you the following terminal output:

There is an error with the value in the string.
The program has now ended.

Notice how on line 7 we have an 'except' statement followed by the word 'ValueError'. This 'ValueError' is an object type that Python produces from the 'int' function on line 4 if it fails to perform the operation.

Because we're using this error type name for our except block, Python will execute this block on line 7 because the error matches the type we want to handle.

Now notice how we have another except block on line 9 which has no error type specified. This block is generic and will only execute if Python did not handle the error in a previous specific except block.

Else And Finally

There are 2 more blocks that we can use in a try/except block. They are 'else' and 'finally'. Let's see a code example with these added:

text = "1234A"

try:
    converted = int(text)
except ValueError:
    print("There is an error with the value in the string.")
except:
    print("An error occurred, but we don't know what.")
else:
    print("The converted number is", converted)
finally:
    print("We have finished trying to convert the text.")

print("The program has now ended.")

Run the code and we get the following terminal output:

There is an error with the value in the string.
We have finished trying to convert the text.
The program has now ended.

First let's explain the 'else' block on line 9. The 'else' must come after 'except' blocks and will only execute if no errors occurred. We don't see the print statement on line 10 in the terminal because we're still producing the conversion error on line 4 so this else block does not execute.

The 'finally' block on line 11 comes after all other block types and will always execute regardless if an error occurred or not.

Try fixing the error now on line 1 by removing the letter 'A' from the string. Notice which print statements now appear in the terminal. Most notably, your 'else' block on line 9 will execute.

Raising Errors

Our final point of learning about error handling is the ability to raise your own errors to interrupt the flow of the program.

Raising your own errors is good when writing code that other people consume. They will be forced to use 'try/except' blocks around using your code which makes them more aware of problems that can occur if they use your code in a bad way.

The following code shows how we can raise our own errors:

item_cost = 500
balance = 300

try:
    if item_cost > balance:
        raise Exception("Insufficient funds in account.")

    balance -= item_cost
except:
    print("Failed to place order.")

Running this code will produce the following terminal output:

Failed to place order.

On line 6 you will see the new keyword 'raise' that causes the 'except' block on line 9 to execute. There is a slight problem though...The raise statement on line 6 specified an error message that we don't use at all. Let's change the except block to handle getting this message value and printing it to the terminal:

item_cost = 500
balance = 300

try:
    if item_cost > balance:
        raise Exception("Insufficient funds in account.")

    balance -= item_cost
except Exception as e:
    print("Failed to place order. The error is:", e)

Now the terminal output includes the message from our raised exception:

Failed to place order. The error is: Insufficient funds in account.

This is achieved by using the 'as' keyword on line 9 followed by a variable name which we have named 'e'. This variable contains the error that was raised on line 6. We pass this variable in to the print statement on line 10 which now correctly prints the message that we raised on line 6.

Custom Error Types

The code example we've used above was simply checking an item cost against a balance value and raising an error if the balance is too low. Imagine in this order process that the code also checked if the item is in stock. If the item is not in stock, we might want to raise another error. But how do we differentiate between insufficient funds and no stock errors?

Python allows us to create our own custom error types using classes which then lets us raise these different types which then allows us to write except blocks for each type. Let's extend the code example to do exactly this:

item_cost = 200
stock = 1
balance = 300

class InsufficientFundsError(Exception):
    pass

class OutOfStockError(Exception):
    pass

try:
    if item_cost > balance:
        raise InsufficientFundsError()

    if stock == 0:
        raise OutOfStockError()

    balance -= item_cost
    stock -= 1
except InsufficientFundsError:
    print("Not enough money in account.")
except OutOfStockError:
    print("Item is out of stock.")
except:
    print("An unknown error has occurred.")
else:
    print("The order was placed successfully.")

Try playing with the variables on lines 1-3 to vary the output in the terminal.

You will see on line 5 and line 8 we define two new classes that use the type 'Exception' as their base. This allows us to use these new type name when we raise errors. We use these 2 new types on lines 13 and line 16.

Being able to raise new error types means we can write and handle different errors. So on line 20, you'll notice how we handle the 'InsufficientFundsError' and line 22 is the except block for handling the 'OutOfStockError'. Being able to have separate blocks for these errors means ultimately we can communicate these different errors to the user, but more importantly, you have a point where your software can behave differently depending on the error type.

What We Learned