The Setting
I often have trouble determining when and how to use exceptions. Let's consider a simple example: suppose I am scraping a webpage, say "http://www.abevigoda.com/", to determine if Abe Vigoda is still alive. To do this, all we need to do is download the page and look for times that the phrase "Abe Vigoda" appears. We return the first appearance, since that includes Abe's status. Conceptually, it will look like this:
def get_abe_status(url):
# download the page
page = download_page(url)
# get all mentions of Abe Vigoda
hits = page.find_all_mentions("Abe Vigoda")
# parse the first hit for his status
status = parse_abe_status(hits[0])
# he's either alive or dead
return status == "alive"
Where parse_abe_status(s)
takes a string of the form "Abe Vigoda is something" and returns the "something" part.
Before you argue that there are much better and more robust ways of scraping this page for Abe's status, remember that this is just a simple and contrived example used to highlight a common situation I'm in.
Now, where can this code encounter problems? Among other errors, some "expected" ones are:
download_page
might not be able to download the page, and throws anIOError
.- The URL might not point to the right page, or the page is downloaded incorrectly, and so there are no hits.
hits
is the empty list, then. - The web page has been altered, possibly making our assumptions about the page wrong. Maybe we expect 4 mentions of Abe Vigoda, but now we find 5.
- For some reasons,
hits[0]
might not be a string of the form "Abe Vigoda is something", and so it cannot be correctly parsed.
The first case isn't really a problem for me: an IOError
is thrown and can be handled by the caller of my function. So let's consider the other cases and how I might handle them. But first, let's assume that we implement parse_abe_status
in the stupidest way possible:
def parse_abe_status(s):
return s[13:]
Namely, it doesn't do any error checking. Now, on to the options:
Option 1: Return None
I can tell the caller that something went wrong by returning None
:
def get_abe_status(url):
# download the page
page = download_page(url)
# get all mentions of Abe Vigoda
hits = page.find_all_mentions("Abe Vigoda")
if not hits:
return None
# parse the first hit for his status
status = parse_abe_status(hits[0])
# he's either alive or dead
return status == "alive"
If the caller receives None
from my function, he should assume that there were no mentions of Abe Vigoda, and so something went wrong. But this is pretty vague, right? And it doesn't help the case where hits[0]
isn't what we thought it was.
On the other hand, we can put in some exceptions:
Option 2: Using Exceptions
If hits
is empty, an IndexError
will be thrown when we attempt hits[0]
. But the caller shouldn't be expected to handle an IndexError
thrown by my function, since he has no idea where that IndexError
came from; it could have been thrown by find_all_mentions
, for all he knows. So we'll create a custom exception class to handle this:
class NotFoundError(Exception):
"""Throw this when something can't be found on a page."""
def get_abe_status(url):
# download the page
page = download_page(url)
# get all mentions of Abe Vigoda
hits = page.find_all_mentions("Abe Vigoda")
try:
hits[0]
except IndexError:
raise NotFoundError("No mentions found.")
# parse the first hit for his status
status = parse_abe_status(hits[0])
# he's either alive or dead
return status == "alive"
Now what if the page has changed and there are an unexpected number of hits? This isn't catastrophic, as the code may still work, but a caller might want to be extra careful, or he might want to log a warning. So I'll throw a warning:
class NotFoundError(Exception):
"""Throw this when something can't be found on a page."""
def get_abe_status(url):
# download the page
page = download_page(url)
# get all mentions of Abe Vigoda
hits = page.find_all_mentions("Abe Vigoda")
try:
hits[0]
except IndexError:
raise NotFoundError("No mentions found.")
# say we expect four hits...
if len(hits) != 4:
raise Warning("An unexpected number of hits.")
logger.warning("An unexpected number of hits.")
# parse the first hit for his status
status = parse_abe_status(hits[0])
# he's either alive or dead
return status == "alive"
Lastly, we might find that status
isn't either alive or dead. Maybe, for some odd reason, today it turned out to be comatose
. Then I don't want to return False
, as that implies that Abe is dead. What should I do here? Throw an exception, probably. But what kind? Should I create a custom exception class?
class NotFoundError(Exception):
"""Throw this when something can't be found on a page."""
def get_abe_status(url):
# download the page
page = download_page(url)
# get all mentions of Abe Vigoda
hits = page.find_all_mentions("Abe Vigoda")
try:
hits[0]
except IndexError:
raise NotFoundError("No mentions found.")
# say we expect four hits...
if len(hits) != 4:
raise Warning("An unexpected number of hits.")
logger.warning("An unexpected number of hits.")
# parse the first hit for his status
status = parse_abe_status(hits[0])
if status not in ['alive', 'dead']:
raise SomeTypeOfError("Status is an unexpected value.")
# he's either alive or dead
return status == "alive"
Option 3: Somewhere in between
I think that the second method, with exceptions, is preferable, but I'm not sure if I'm using exceptions correctly within it. I'm curious to see how more experienced programmers would handle this.