I've been wondering about some of the standards that people use when handling errors. I think I've found a setup that will help everyone out.
Here's some of the things I will be going over.
- Error Types
- Handling your errors.
- Handling user errors.
- and finally the dos and don'ts.
Let's talk about error types. There's essentially 2 scenarios when handling errors: expected errors and unexpected errors Expected errors are exactly what you'd expect. Literally. Most of these errors come from invalid user input. Frequently in the web world these turn into 400-level HTTP status codes. Unexpected errors are normally caused by something the programming did wrong. In HTTP these usually become 500 errors.
In the best case scenario we would want to be able to enumerate and understand every error possible. In reality there is only so much we can anticipate. At some point we have to make a decision about how many and what types of errors we understand and devise a strategy for handling them.
Handling Expected Errors
How do we handle expected (read: user) errors? This is the easier of the two categories. As I mentioned earlier many of these errors are from user input. We should have a good idea of what a user can do. One good example is logging in to an account. The user must provide a username and password and if they input something wrong then that's an error. Many times when handling this you want to give the user useful information. We don't want to be spitting out a cryptic error message and stacktrace. Most of the time we want to have a good grasp on exactly what they did wrong and inform them of it in a way that helps them do the right thing. A simple example of this would be someone inputs the wrong username and we return that username does not exist. It's better still to take the additional step to recommend they check their spelling. This informs the user exactly what was wrong and some steps they can do to fix the problem.
Handling Unexpected Errors
I try very hard to write really good code. I do. But perfect code remains elusive and somehow those pesky users get my code into these unimaginable states causing unspeakable, unheardof errors. One way we could combat this is to go around putting try/catches around everything attempting to handle every single error that could ever exist. The amount of work that would take is immense and in reality unnecessary. We need to accept the fact that we can't prevent every problem. Once we accept that we find the real solution: let the errors happen and gather as much as can about the error. Later we can use that information to harden our software against the error. What we want is for the error to be captured in an exception and 'bubbled to the top'. The purpose of this technique is to store the information about the error and pass it to the highest level of the program execution. We don't want our users seeing a stack trace with the all the error information, so once it reaches the top we catch the error and just inform the user that unfortunately something bad happened that we don't fully understand. All the while we are storing the information about what happened for ourselves and informing everyone responsible that something unexpected and bad happened. Optionally the program can reach out to the customer/user that encountered the problem and get some information from them about what they were doing when the error occurred. This can help developers later to reproduce the problem to better understand it.
This is a list of things you want to strive for when figuring out how to display or handle your errors.
Reproducibility - The ability to reproduce the problem that occurred or call the error that happened.
Reuse - The ability for another to use your code and figure out what’s going on.
Future Bug Fixes - You want to make it as easy as possible to write in future fixes into your code.
Readability - You want someone reading the code to be able to understand why and how.
Log All the Things - Store as much info as possible about the error that occurred to help with finding out what the problem was.
- Silencing Errors
try: find_user(user) except: pass
In this example we are running our
find_user function which will find information on a user. If this process was to fail then nothing would happen. Nothing would succeed but we wouldn't really know if it failed either. This is a definite "don't" because we have no idea what happened and neither does the user. We have no information regarding the error and even that an error occurred.
- Masking Errors
try: find_user(user) except: raise UserNotFound
In this example we take any exception that would come from
find_user function and replace it with
UserNotFound. This is bad because there are many kinds of exceptions that could be raised that have nothing to do with the user not being found. What if the table doesn't exist for some reason? What if the system is out of memory? What if the hard drive fell off? Sure this shows us that an error occurred but translating the exception without gathering data about the origin could lead us down the incorrect path when we work on building a fix.
Frankly many people use try/catch’s given there are many situations when they are useful. I tend to avoid them because of the information they leave out when handling errors. This could be from the real error being masked or simply not being returned.
I believe this has many helpful ways to make solving bugs much easier for you and your team. There are many more ways to handle errors in a good manner but I hope you learned something from what I've shown you today. As always thanks for stopping by at a Layer0, hope you guys have a wonderful day and stay awesome.