Chapter 11. Exception Handling: What to Do When Things Go Wrong

image with no caption

Things go wrong, all the time—no matter how good your code is.

You’ve successfully executed all of the examples in this book, and you’re likely confident all of the code presented thus far works. But does this mean the code is robust? Probably not. Writing code based on the assumption that nothing bad ever happens is (at best) naive. At worst, it’s dangerous, as unforeseen things do (and will) happen. It’s much better if you’re wary while coding, as opposed to trusting. Care is needed to ensure your code does what you want it to, as well as reacts properly when things go south. In this chapter, you’ll not only see what can go wrong, but also learn what to do when (and, oftentimes, before) things do.

Databases Aren’t Always Available

We’ve identified four potential issues with the vsearch4web.py code, and we concede that there may be many more, but we’ll worry about these four issues for now. Let’s consider each of the four issues in more detail (which we do here and on the next few pages, by simply describing the problems; we’ll work on solutions later in this chapter). First up is worrying about the backend database:

  1. What happens if the database connection fails?

    Our webapp blissfully assumes that the backend database is always operational and available, but it may not be (for any number of reasons). At the moment, it is unclear what happens when the database is down, as our code does not consider this eventuality.

Let’s see what happens if we temporarily switch off the backend database. As you can see below, our webapp loads fine, but as soon as we do anything, an intimidating error message appears:

image with no caption

Web Attacks Are a Real Pain

As well as worrying about issues with your backend database, you also need to worry about nasty individuals trying to do nasty things to your webapp, which brings us to the second issue:

  1. Is our webapp protected from web attacks?

    The phrases SQL injection (SQLi) and Cross-site scripting (XSS) should strike fear in the heart of every web developer. The former allows attackers to exploit your backend database, while the latter allows them to exploit your website. There are other web exploits that you’ll need to worry about, but these are the “big two.”

As with the first issue, let’s see what happens when we try to simulate these exploits against our webapp. As you can see, it appears we’re ready for both of them:

image with no caption

Input-Output Is (Sometimes) Slow

At the moment, our webapp communicates with our backend database in an almost instantaneous manner, and users of our webapp notice little or no delay as the webapp interacts with the database. But imagine if the interactions with the backend database took some time, perhaps seconds:

  1. What happens if something takes a long time?

    Perhaps the backend database is on another machine, in another building, on another continent...what would happen then?

Communications with the backend database may take time. In fact, whenever your code has to interact with something that’s external to it (for example: a file, a database, a network, or whatever), the interaction can take any amount of time, the determination of which is usually beyond your control. Despite this lack of control, you do have to be cognizant that some operations may be lengthy.

To demonstrate this issue, let’s add an artificial delay to our webapp (using the sleep function, which is part of the standard library’s time module). Add this line of code to the top of your webapp (near the other import statements):

from time import sleep

With the above import statement inserted, edit the log_request function and insert this line of code before the with statement:

sleep(15)

If you restart your webapp, then initiate a search, there’s a very distinct delay while your web browser waits for your webapp to catch up. As web delays go, 15 seconds will feel like a lifetime, which will prompt most users of your webapp to believe something has crashed:

image with no caption

Your Function Calls Can Fail

The final issue identified during this chapter’s opening exercise relates to the function call to log_request within the do_search function:

  1. What happens if a function call fails?

    There’s never a guarantee that a function call will succeed, especially if the function in question interacts with something external to your code.

We’ve already seen what can happen when the backend database is unavailable— the webapp crashes with an InterfaceError:

image with no caption

Other problems can surface, too. To simulate another error, find the sleep(15) line you added from the Issue 3 discussion, and replace it with a single statement: raise. When executed by the interpreter, raise forces a runtime error. If you try your webapp again, a different error occurs this time:

image with no caption

Note

Before flipping the page, remove that call to “raise” from your code to ensure the webapp starts working again.

Considering the Identified Problems

We’ve identified four issues with the vsearch4web.py code. Let’s revisit each and consider our next steps.

1. Your database connection fails

Errors occur whenever an external system your code relies on is unavailable. The interpreter reported an InterfaceError when this happened. It’s possible to spot, then react to, these types of errors using Python’s built-in exception-handling mechanism. If you can spot when an error occurs, you’re then in a position to do something about it.

Geek Bits

image with no caption

If you want to know more about SQLi and XSS, Wikipedia is a great place to start. See https://en.wikipedia.org/wiki/SQL_injection and https://en.wikipedia.org/wiki/Cross-site_scripting, respectively. And remember, there are all kinds of other types of attack that can cause problems for your app; these are just the two biggies.

2. Your application is subjected to an attack

Although a case can be made that worrying about attacks on your application is only of concern to web developers, developing practices that improve the robustness of the code you write are always worth considering. With vsearch4web.py, dealing with the “big two” web attack vectors, SQL injection (SQLi) and Cross-site scripting (XSS), appears to be well in hand. This is more of a happy accident than by design on your part, as the Jinja2 library is built to guard against XSS by default, escaping any potentially problematic strings (recall that the JavaScript we tried to trick our webapp into executing had no effect). As regards SQLi, our use of DB-API’s parameterized SQL strings (with all those %s placeholders) ensures—again, thanks to the way these modules were designed—that your code is protected from this entire class of attack.

3. Your code takes a long time to execute

If your code takes a long time to execute, you have to consider the impact on your user’s experience. If your user doesn’t notice, then you’re likely OK. However, if your user has to wait, you may have to do something about it (otherwise, your user may decide the wait isn’t worth it, and go elsewhere).

4. Your function call fails

It’s not just external systems that generate exceptions in the interpreter—your code can raise exceptions, too. When this happens, you need to be ready to spot the exception, then recover as needed. The mechanism you use to enable this behavior is the same one hinted at in the discussion of issue 1, above.

So...where do we start when dealing with these four issues? It’s possible to use the same mechanism to deal with issues 1 and 4, so that’s where we’ll begin.

Always Try to Execute Error-Prone Code

When something goes wrong with your code, Python raises a runtime exception. Think of an exception as a controlled program crash triggered by the interpreter.

As you’ve seen with issues 1 and 4, exceptions can be raised under many different circumstances. In fact, the interpreter comes with a whole host of built-in exception types, of which RuntimeError (from issue 4) is only one example. As well as the built-in exception types, it’s possible to define your own custom exceptions, and you’ve seen an example of this too: the InterfaceError exception (from issue 1) is defined by the MySQL Connector module.

To spot (and, hopefully, recover from) a runtime exception, deploy Python’s try statement, which can help you manage exceptions as they occur at runtime.

To see try in action, let’s first consider a snippet of code that might fail when executed. Here are three innocent-looking, but potentially problematic, lines of code for you to consider:

For a complete list of the built-in exceptions, see https://docs.python.org/3/library/exceptions.html.

image with no caption

There’s nothing wrong with these three lines of code and—as currently written— they will execute. However, this code might fail if it can’t access myfile.txt. Perhaps the file is missing, or your code doesn’t have the necessary file-reading permissions. When the code fails, an exception is raised:

image with no caption

Let’s start learning what try can do by adjusting the above code snippet to protect against this FileNotFoundError exception.

Catching an Error Is Not Enough

When a runtime error occurs, an exception is raised. If you ignore a raised exception it is referred to as uncaught, and the interpreter will terminate your code, then display a runtime error message (as shown in the example from the bottom of the last page). That said, raised exceptions can also be caught (i.e., dealt with) with the try statement. Note that it’s not enough to catch runtime errors, you also have to decide what you’re going to do next.

Perhaps you’ll decide to deliberately ignore the raised exception, and keep going...with your fingers firmly crossed. Or maybe you’ll try to run some other code in place of the code that crashed, and keep going. Or perhaps the best thing to do is to log the error before terminating your application as cleanly as possible. Whatever you decide to do, the try statement can help.

In its most basic form, the try statement allows you to react whenever the execution of your code results in a raised exception. To protect code with try, put the code within try’s suite. If an exception occurs, the code in the try’s suite terminates, and then the code in the try’s except suite runs. The except suite is where you define what you want to happen next.

Let’s update the code snippet from the last page to display a short message whenever the FileNotFoundError exception is raised. The code on the left is what you had previously, while the code on the right has been amended to take advantage of what try and except have to offer:

When a runtime error is raised, it can be caught or uncaught: “try” lets you catch a raised error, and “except” lets you do something about it.

image with no caption

Note that what was three lines of code is now six, which may seem wasteful, but isn’t. The original snippet of code still exists as an entity; it makes up the suite associated with the try statement. The except statement (and its associated suite) is new code. Let’s see what difference these amendments make.

There can be more than one exception raised...

This new behavior is better, but what happens if myfile.txt exists but your code does not have permission to read from it? To see what happens, we created the file, then set its permissions to simulate this eventuality. Rerunning the new code produces this output:

image with no caption

try Once, but except Many Times

To protect against another exception being raised, simply add another except suite to your try statement, identifying the exception you’re interested in and providing whatever code you deem necessary in the new except’s suite. Here’s another updated version of the code that handles the PermissionError exception (should it be raised):

image with no caption

Executing this amended code still results in the PermissionError exception being raised. However, unlike before, the ugly traceback has been replaced by a much friendlier message:

image with no caption

This is looking good: you’ve managed to adjust what happens whenever the file you’re hoping to work with isn’t there (it doesn’t exist), or is inaccessible (you don’t have the correct permissions). But what happens if an exception is raised that you weren’t expecting?

A Lot of Things Can Go Wrong

Before answering the question posed at the bottom of the last page—what happens if an exception is raised that you weren’t expecting?—take a look at some of Python 3’s built-in exceptions (which are copied directly from the Python documentation). Don’t be surprised if you’re struck by just how many there are:

image with no caption

It would be crazy to try to write a separate except suite for each of these runtime exceptions, as some of them may never occur. That said, some might occur, so you do need to worry about them a little bit. Rather than try to handle each exception individually, Python lets you define a catch-all except suite, which fires whenever a runtime exception occurs that you haven’t specifically identified.

The Catch-All Exception Handler

Let’s see what happens when some other error occurs. To simulate just such an occurrence, we’ve changed myfile.txt from a file into a folder. Let’s see what happens when we run the code now:

image with no caption

Another exception is raised. You could create an extra except suite that fires when this IsADirectoryError exception occurs, but let’s specify a catch-all runtime error instead, which fires whenever any exception (other than the two we’ve already specified) occurs. To do this, add a catch-all except statement to the end of the existing code:

image with no caption

Running this amended version of your code gets rid of the ugly traceback, displaying a friendly message instead. No matter what other exception occurs, this code handles it thanks to the addition of the catch-all except statement:

image with no caption

Haven’t We Just Lost Something?

image with no caption

Ah, yes...good catch.

This latest code has tidied up the output (in that the ugly traceback is gone), but you’ve also lost some important information: you no longer know what the specific issue with your code was.

Knowing what exception was raised is often important, so Python lets you get at the data associated with the most-recent exception information as it’s being handled. There are two ways to do this: using the facilities of the sys module, and using an extension to the try/except syntax.

Let’s look at both of these techniques.

Learning About Exceptions from “sys”

The standard library comes with a module called sys that provides access to the interpreter’s internals (a set of variables and functions available at runtime).

One such function is exc_info, which provides information on the exception currently being handled. When invoked, exc_info returns a three-valued tuple where the first value indicates the exception’s type, the second details the exception’s value, and the third contains a traceback object that provides access to the traceback message (should you need it). When there is no currently available exception, exc_info returns the Python null value for each of the tuple values, which looks like this:(None, None, None).

Knowing all of this, let’s experiment at the >>> shell. In the IDLE session that follows, we’ve written some code that’s always going to fail (as dividing by zero is never a good idea). A catch-all except suite uses the sys.exc_info function to extract and display data relating to the currently firing exception:

To learn more about “sys”, see https://docs.python.org/3/library/sys.html.

image with no caption

It’s possible to delve deeper into the traceback object to learn more about what just happened, but this already feels like too much work, doesn’t it? All we really want to know is what type of exception occurred.

To make this (and your life) easier, Python extends the try/except syntax to make it convenient to get at the information returned by the sys.exc_info function, and it does this without you having to remember to import or interact with the sys module, nor wrangle with the tuple returned by the exc_info function.

Recall from a few pages back that the interpreter arranges exceptions in a hierarchy, with each exception inheriting from one called Exception. Let’s take advantage of this hierarchical arrangement as we rewrite our catch-all exception handler.

image with no caption

The Catch-All Exception Handler, Revisited

Consider your current code, which explicitly identifies the two exceptions you want to handle (FileNotFoundError and PermissionError), as well as provides a generic catch-all except suite (to handle everything else):

image with no caption

Note how, when referring to a specific exception, we’ve identified the exception by name after the except keyword. As well as identifying specific exceptions after except, it’s also possible to identify classes of exceptions using any of the names in the hierarchy.

For instance, if you’re only interested in knowing that an arithmetic error has occurred (as opposed to—specifically—a divide-by-zero error), you could specify except ArithmeticError, which would then catch a FloatingPointError, an OverflowError, and a ZeroDivisionError should they occur. Similarly, if you specify except Exception, you’ll catch any error.

image with no caption

But how does this help...surely you’re already catching all errors with a “bare” except statement? It’s true: you are. But you can extend the except Exception statement with the as keyword, which allows you to assign the current exception object to a variable (with err being a very popular name in this situation) and create more informative error message. Take a look at another version of the code, which uses except Exception as:

image with no caption

Let’s start with confirming that the code displays the correct message when the file is missing:

image with no caption

If the file exists, but you don’t have permission to access it, a different exception is raised:

image with no caption

Any other exception is handled by the catch-all, which displays a friendly message:

image with no caption

Finally, if all is OK, the try suite runs without error, and the file’s contents appear on screen:

image with no caption

Getting Back to Our Webapp Code

Recall from the start of this chapter that we identified an issue with the call to log_request within vsearch4web.py’s do_search function. Specifically, we’re concerned about what to do when the call to log_request fails:

image with no caption

Based on our investigations, we learned that this call might fail if the backend database is unavailable, or if some other error occurs. When an error (of any type) occurs, the webapp responds with an unfriendly error page, which is likely to confuse (rather than enlighten) your webapp’s users:

image with no caption

Although it is important to us, the logging of each web request is not something that our webapp users really care about; all they want to see is the results of their search. Consequently, let’s adjust the webapp’s code so that it deals with errors within log_request by handling any raised exceptions silently.

Silently Handling Exceptions

image with no caption

No: “silently” does not mean “ignore.”

When we suggest handling exceptions silently in this context, we’re referring to handling any exceptions raised in such a way that your webapp users don’t notice. At the moment, your users do notice, as the webapp crashes with a confusing and—let’s be honest—scary error page.

Your webapp users don’t need to worry about log_request failing, but you do. So let’s adjust your code so that exceptions raised by log_request aren’t noticed by your users (i.e., they are silenced), but are noticed by you.

Handling Other Database Errors

The log_request function makes use of the UseDatabase context manager (as provided by the DBcm module). Now that you’ve protected the call to log_request, you can rest easy, safe in the knowledge that any issues relating to problems with the database will be caught (and handled) by your catch-all exception-handling code.

However, the log_request function isn’t the only place where your webapp interacts with the database. The view_the_log function grabs the logging data from the database prior to displaying it on screen.

Recall the code for the view_the_log function:

image with no caption

This code can fail, too, as it interacts with the backend database. However, unlike log_request, the view_the_log function is not called from the code in vsearch4web.py; it’s invoked by Flask on your behalf. This means you can’t write code to protect the invocation of view_the_log, as it’s the Flask framework that calls the function, not you.

If you can’t protect the invocation of view_the_log, the next best thing is to protect the code in its suite, specifically the use of the UseDatabase context manager. Before considering how to do this, let’s consider what can go wrong:

  • The backend database may be unavailable.

  • You may not be able to log in to a working database.

  • After a successful login, your database query might fail.

  • Something else (unexpected) might happen.

This list of problems is similar to those you had to worry about with log_request.

Does “More Errors” Mean “More excepts”?

Knowing what we now know about try/except, we could add some code to the view_the_log function to protect the use of the UseDatabase context manager:

image with no caption

This catch-all strategy certainly works (after all, that’s what you used with log_request). However, things can get complicated if you decide to do something other than implement a catch-all exception handler. What if you decide you need to react to a specific database error, such as “Database not found”? Recall from the beginning of this chapter that MySQL reports an InterfaceError exception when this happens:

image with no caption

You could add an except statement that targets the InterfaceError exception, but to do this your code also has to import the mysql.connector module, which defines this particular exception.

On the face of things, this doesn’t seem like a big deal. But it is.

Avoid Tightly Coupled Code

Let’s assume you’ve decided to create an except statement that protects against your backend database being unavailable. You could adjust the code in view_the_log to look something like this:

image with no caption

If you also remember to add import mysql.connector to the top of your code, this additional except statement works. When your backend database can’t be found, this additional code allows your webapp to remind you to check that your database is switched on.

This new code works, and you can see what’s going on here...what’s not to like?

The issue with approaching the problem in this way is that the code in vsearch4web.py is now very tightly coupled to the MySQL database, and specifically the use of the MySQL Connector module. Prior to adding this second except statement, your vsearch4web.py code interacted with your backend database via the DBcm module (developed earlier in this book). Specifically, the UseDatabase context manager provides a convenient abstraction that decouples the code in vsearch4web.py from the backend database. If, at some time in the future, you needed to replace MySQL with PostgreSQL, the only changes you’d need to make would be to the DBcm module, not to all the code that uses UseDatabase. However, when you create code like that shown above, you tightly bind (i.e., couple) your webapp code to the MySQL backend database because of that import mysql.connector statement, in addition to your new except statement’s reference to mysql.connector.errors.InterfaceError.

image with no caption

If you need to write code that tightly couples to your backend database, always consider putting that code in the DBcm module. This way, your webapp can be written to use the generic interface provided by DBcm, as opposed to a specific interface that targets (and locks you into) a specific backend database.

Let’s now consider what moving the above except code into DBcm does for our webapp.

The DBcm Module, Revisited

You last looked at DBcm in Chapter 9, when you created that module in order to provide a hook into the with statement when working with a MySQL database. Back then, we sidestepped any discussion of error handling (by conveniently ignoring the issue). Now that you’ve seen what the sys.exc_info function does, you should have a better idea of what the arguments to UseDatabase’s __exit__ method mean:

image with no caption

Recall that UseDatabase implements three methods:

  • __init__ provides a configuration opportunity prior to with executing,

  • __enter__ executes as the with statement starts, and

  • __exit__ is guaranteed to execute whenever the with’s suite terminates.

At least, that’s the expected behavior whenever everything goes to plan. When things go wrong, this behavior changes.

For instance, if an exception is raised while __enter__ is executing, the with statement terminates, and any subsequent processing of __exit__ is cancelled. This makes sense: if __enter__ runs into trouble, __exit__ can no longer assume that the execution context is initialized and configured correctly (so it’s prudent not to run the __exit__ method’s code).

The big issue with the __enter__ method’s code is that the backend database may not be available, so let’s take some time to adjust __enter__ for this possibility, generating a custom exception when the database connection cannot be established. Once we’ve done this, we’ll adjust view_the_log to check for our custom exception instead of the highly database-specific mysql.connector.errors.InterfaceError.

Creating Custom Exceptions

Creating your own custom exceptions couldn’t be any easier: decide on an appropriate name, then define an empty class that inherits from Python’s built-in Exception class. Once you’ve defined a custom exception, it can be raised with the raise keyword. And once an exception is raised, it’s caught (and dealt with) by try/except.

A quick trip to IDLE’s >>> prompt demonstrates custom exceptions in action. In this example, we’re creating a custom exception called ConnectionError, which we then raise (with raise), before catching with try/except. Read the annotations in numbered order, and (if you’re following along) enter the code we’ve typed at the >>> prompt:

image with no caption

The empty class isn’t quite empty...

In describing the ConnectionError class as “empty,” we told a little lie. Granted, the use of pass ensures that there’s no new code associated with the ConnectionError class, but the fact that ConnectionError inherits from Python’s built-in Exception class means that all of the attributes and behaviors of Exception are available in ConnectionError too (making it anything but empty). This explains why ConnectionError works just as you’d expect it to with raise and try/except.

What Else Can Go Wrong with “DBcm”?

Even if your backend database is up and running, things can still go wrong.

For example, the credentials used to access the database may be incorrect. If they are, the __enter__ method will fail again, this time with a mysql.connector.errors.ProgrammingError.

Or, the suite of code associated with your UseDatabase context manager may raise an exception, as there’s never a guarantee that it executes correctly. A mysql.connector.errors.ProgrammingError is also raised whenever your database query (the SQL you’re executing) contains an error.

The error message associated with an SQL query error is different than the message associated with the credentials error, but the exception raised is the same: mysql.connector.errors.ProgrammingError. Unlike with credentials errors, errors in your SQL results in an exception being raised while the with statement is executing. This means that you’ll need to consider protecting against this exception in more than one place. The question is: where?

To answer this question, let’s take another look at DBcm’s code:

image with no caption

You might be tempted to suggest that exceptions raised within the with suite should be handled with a try/except statement within the with, but such a strategy gets you right back to writing tightly coupled code. But consider this: when an exception is raised within with’s suite and not caught, the with statement arranges to pass details of the uncaught exception into your context manager’s __exit__ method, where you have the option of doing something about it.

Creating More Custom Exceptions

Let’s extend DBcm.py to report two additional, custom exceptions.

The first is called CredentialsError and is raised when a ProgrammingError occurs within the __enter__ method. The second is called SQLError and is raised when a ProgrammingError is reported to the __exit__ method.

Defining these new exceptions is easy: add two new, empty exception classes to the top of DBcm.py:

image with no caption

A CredentialsError can occur during __enter__, so let’s adjust that method’s code to reflect this. Recall that an incorrect MySQL username or password results in a ProgrammingError being raised:

image with no caption

These code changes adjust DBcm.py to raise a CredentialsError exception when you provide either an incorrect username or password from your code to your backend database (MySQL). Adjusting vsearch4web.py’s code is your next task.

Are Your Database Credentials Correct?

With these latest changes made to DBcm.py, let’s now adjust the code in vsearch4web.py, paying particular attention to the view_the_log function. However, before doing anything else, add CredentialsError to the list of imports from DBcm at the top of your vsearch4web.py code:

image with no caption

With the import line amended, you next need to add a new except suite to the view_the_log function. As when you added support for a ConnectionError, this is a straightforward edit:

image with no caption

There’s really nothing new here, as all you’re doing is repeating what you did for ConnectionError. Sure enough, if you try to connect to your backend database with an incorrect username (or password), your webapp now displays an appropriate message, like this:

image with no caption

Handling SQLError Is Different

Both ConnectionError and CredentialsError are raised due to problems with the __enter__ method’s code executing. When either exception is raised, the corresponding with statement’s suite is not executed.

If all is well, your with suite executes as normal.

Recall this with statement from the log_request function, which uses the UseDatabase context manager (provided by DBcm) to insert data into the backend database:

image with no caption

If (for some reason) your SQL query contains an error, the MySQL Connector module generates a ProgrammingError, just like the one raised during your context manager’s __enter__ method. However, as this exception occurs within your context manager (i.e., within the with statement) and is not caught there, the exception is passed back to the __exit__ method as three arguments: the type of the exception, the value of the exception, and the traceback associated with the exception.

If you take a quick look at DBcm’s existing code for __exit__, you’ll see that the three arguments are ready and waiting to be used:

image with no caption

When an exception is raised within the with suite and not caught, the context manager terminates the with suite’s code, and jumps to the __exit__ method, which then executes. Knowing this, you can write code that checks for exceptions of interest to your application. However, if no exception is raised, the three arguments (exc_type, exc_value, and exc_traceback) are all set to None. Otherwise, they are populated with details of the raised exception.

“None” is Python’s null value.

Let’s exploit this behavior to raise an SQLError whenever something goes wrong within the UseDatabase context manager’s with suite.

Be Careful with Code Positioning

To check whether an uncaught exception has occurred within your code’s with statement, check the exc_type argument to the __exit__ method within __exit__’s suite, being careful to consider exactly where you add your new code.

image with no caption

Yes, it does make a difference.

To understand why, consider that your context manager’s __exit__ method provides a place where you can put code that is guaranteed to execute after your with suite ends. That behavior is part of the context manager protocol, after all.

This behavior needs to hold even when exceptions are raised within your context manager’s with suite. Which means that if you plan to add code to the __exit__ method, it’s best to put it after any existing code in __exit__, as that way you’ll still guarantee the method’s existing code executes (and preserve the semantics of the context manager protocol).

Let’s take another look at the existing code in the __exit__ method in light of this code placement discussion. Consider that any code we add needs to raise an SQLError exception if exc_type indicates a ProgrammingError has occurred:

image with no caption

Raising an SQLError

At this stage, you’ve already added the SQLError exception class to the top of the DBcm.py file:

image with no caption

With the SQLError exception class defined, all you need to do now is add some code to the __exit__ method to check whether exc_type is the exception you’re interested in, and if it is, raise an SQLError. This is so straightforward that we are resisting the usual Head First urge to turn creating the required code into an exercise, as no one wants to insult anyone’s intelligence at this stage in this book. So, here’s the code you need to append to the __exit__ method:

image with no caption

If you want to be extra safe, and do something sensible with any other, unexpected exception sent to __exit__, you can add an elif suite to the end of the __exit__ method that reraises the unexpected exception to the calling code:

image with no caption

A Quick Recap: Adding Robustness

Let’s take a minute to remind ourselves of what we set out to do in this chapter. In attempting to make our webapp code more robust, we had to answer four questions relating to four identified issues. Let’s review each question and note how we did:

  1. What happens if the database connection fails?

    You created a new exception called ConnectionError that is raised whenever your backend database can’t be found. You then used try/except to handle a ConnectionError were it to occur.

  2. Is our webapp protected from web attacks?

    It was a “happy accident,” but your choice of Flask plus Jinja2, together with Python’s DB-API specification, protects your webapp from the most notorious of web attacks. So, yes, your webapp is protected from some web attacks (but not all).

  3. What happens if something takes a long time?

    We still haven’t answered this question, other than to demonstrate what happens when your webapp takes 15 seconds to respond to a user request: your web user has to wait (or, more likely, your web user gets fed up waiting and leaves).

  4. What happens if a function call fails?

    You used try/except to protect the function call, which allowed you to control what the user of your webapp sees when something goes wrong.

What happens if something takes a long time?

When you did the initial exercise at the start of this chapter, this question resulted from our examination of the cursor.execute calls that occurred in the log_request and view_the_log functions. Although you’ve already worked with both of these functions in answering questions 1 and 4, above, you’re not done with them quite yet.

Both log_request and view_the_log use the UseDatabase context manager to execute an SQL query. The log_request function writes the details of the submitted search to the backend database, whereas the view_the_log function reads from the database.

The question is: what do you do if this write or read takes a long time?

Well, as with a lot of things in the programming world, it depends.

How to Deal with Wait? It Depends...

How you decide to deal with code that makes your users wait—either on a read, or on a write—can get complex. So we’re going to pause this discussion and defer a solution until the next, short chapter.

In fact, the next chapter is so short that it doesn’t warrant its own chapter number (as you’ll see), but the material it presents is complex enough to justify splitting it off from this chapter’s main discussion, which presented Python’s try/except mechanism. So, let’s hang on for a bit before putting to rest issue 3: what happens if something takes a long time?

image with no caption

Yes. The irony is not lost on us.

We’re asking you to wait to learn how to handle “waits” in your code.

But you’ve already learned a lot in this chapter, and we think it’s important to take a bit of time to let the try/except material sink into your brain.

So, we’d like you to pause, and take a short break...after you’ve cast your eye over the code seen thus far in this chapter.

Chapter 11’s Code, 1 of 3

image with no caption
image with no caption

Chapter 11’s Code, 2 of 3

image with no caption

Chapter 11’s Code, 3 of 3

image with no caption
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset