# Exceptions

The hardest thing about programming is error handling. It's easy to write a
program which works well when your data is going down the golden path which you
laid out for it, but when there's something unexpected all hell can break
loose. Errors can range from failing loudly, failing silently, silently
overwriting important data, to crashing your computer. Given the choice I'd
take the first one every time (too often at my job errors are of the last
kind).

Traditionally, languages like C could only return one variable from a function.
Often a special value of that variable was chosen. This is sometimes called
'in-band error handling' and can be problematic because it's way to easy to
reuse that special value, or forget to check it.

```python
def do_work():
    # Work
    if work_was_successful:
        return result
    else
        return -1

result = do_work()
if result == -1:
    # Handle error
    pass

do_work()
# Oops, forgot to check the status.
```

Python takes a different approach to error handling. It has special errors
called exceptions. Exceptions have to be dealt with, also known as caught,
otherwise the function will exit. If the calling function doesn't deal with the
exception it will bubble up all the way to the top and kill the program. This
follows the "Explicit is better than implicit" mentality and keeps you from
screwing up silently. Because you aren't using the return value to both
indicate the result of the function, and whether there was an error, this is
known as "out of band" error handling.

Exceptions can be raised (sometimes also called thrown in other languages like
Java), using the `raise` keyword. Anything can be raised as an exception, but
it's best if you create an explicit type of exception, or reuse one of the
built in types of exceptions.

```python
raise 'A string message'

class MySpecialPurposeException(Exception):
    # Inherits from the base Exception.
    pass

raise MySpecialPurposeException('A message')

raise NameError # Seen any of exceptions like this?
```

Now that we know how to raise exceptions, how can we deal with them? We can use
a special construct called a `try`/`catch` or `try`/`except` block (again, the
names change slightly depending on the language, and you will hear both)

```python
try:
    f = open('some_file_which_may_not_exist', 'r')
    print(f.read())
except IOError as io_exception:
    print('An exception was thrown', io_exception)
```

You can also swallow all of the exceptions a function may throw like this:
```python
try:
    f = open('some_file_which_may_not_exist', 'r')
    print(f.read())
except:
    print('Swallow all of the exceptions!')
```
This is considered bad practice because the "Explicit is better than implicit".

Exceptions can also be used as a form of flow control, that is a way of
directing program flow just like if statements and for loops. We'll encounter
this soon enough.