In another topic I posted the line:
The end two options assume you don’t put big try-except statements through your code. If you are, you should probably reconsider why your code is generating exceptions. <rant>For some reasons, throwing and catching exceptions is considered ‘pythonic’, but in my mind, exceptions should be reserved for when there is unexpected behaviour. Pretty much: if you can check first, do, and don’t be afraid to throw exceptions yourself.</rant>
To which there was the response
It is the opposite, you catch expected errors.
Unexpected errors are errors you do not catch = your code is not ready to deal with them.
In other words: When you catch an error it means the error is expected behavior of the surrounded code. The surrounding code knows how to deal with it.
Finally it comes to the same point: do not be afraid to throw exceptions when your code does not follow the “good case”. It is sort of a return value too.
And
Throwing exceptions is sin!!
So let’s discuss it.
***We’ll start by me talking about how and why I use exceptions.
Where do I use Try-Except
I use try except to handle things that cannot be controlled, such as the input and output to files:
try:
new_data = json.load(open(self._path))
except:
try:
json.dump(
self._defaults,
open(self._path, 'w'),
indent=2,
sort_keys=True
)
except:
print("Unable to write file, check permissions")
new_data = self._defaults.copy()
It’s a bit of safety around loading JSON files. If it can’t load them, it will attempt to save the defaults and then use them.
Why do I use them here:
It adds robustness to my program, because it can now deal with unusual/exceptional situations such as missing/corrupted data files or a read only file system, and still carry on working.
A note on the word ‘unexpected’: Clearly I have anticipated the code failing here, but this is still ‘unexpected.’ Why? I would expect that the file is valid as I saved to it earlier - something odd must have happened here, and we can recover from it.
To me this is what try-except’s are about: keeping your code running when odd things are happening. It’s for the cases where the program can operate with a little less functionality in a situation where it formerly couldn’t - because of the error handling.
Where do I not use Try-Except
Code that runs every frame:
try:
property_value = cont.owner['property_name']
except IndexError:
cont.owner['property_name'] = 0
property_value = 0
Instead I would ensure it exists by creating it before I attempt to access it (through an initialisation routine).
Likewise, I would not use cont.owner.get(‘property_name’, 0) here either - unless it is reading configuration set by the user. making this possibly invalid input.
Why do I not use them here?
A game object does not have the properties by default. Thus, it is expected that this code will fail! So why even try the first time? Set up a system so that your code does not ‘fail’ in an expected way.
When do I raise errors:
How about this
def from_dict(source):
if source['type'] not in TYPE_STRINGS:
raise ValueError("Unknown source type {} requested",.format(source['type']))
Again this is part of parsing external input. It is when there are no syntax issues with the input, but rather the operation cannot be completed because the argument value is wrong. In this case it is non-recoverable, so we raise an error. Why not let python raise it’s own error when we do the indexing? Because we want the error to explain a bit more about the problem. A generic IndexError which would be thrown by default would not tell the user what is actually wrong, but by raising it ourself we can add more information.
If the string not being in the dictionary was recoverable, I would not raise the error, but rather log a warning to the file, and continue (presumably using a default value).
When do I use custom errors
When there are issues that need to be conveyed that cannot be encapsulated by one of the default error types. In a non-bge project, I was working with a protocol for talking over serial. A python parser and GUI was written around this protocol, and it used a whole heap of custom errors to deal with malformed headers, invalid checksums etc. These are all ‘unexpected’ behaviour that prevent the code from working, and provide much more information to a person trying to get your code to work.
An interesting use
In a scheduler I have the code:
errors = list()
for event in self.events:
if event.time_until_next_run <= 0:
try:
event(self)
except Exception as err:
errors.append(err)
for err in errors:
raise err
Now why would I do this? Catch every single error, and then a little later, raise them all (or at least, raise the first one)!
The reason is quite simple. A scheduler runs multiple independent bits of code. Just because one part has failed doesn’t mean all of it has. Imagine one AI in your game hits an error - it enters an invalid internal state. Now a whole heap of your other code is not even running, even though it may deal with other things completely, such as player input and GUI handling. So it adds robustness, keeping as much of your code running even though some parts have failed. If this scheduler weren’t in the BGE (which starts again next frame), I’d likely have a system that just logs the errors and then continues.
Summary
It’s much easier to not ‘bug’ in the first place than it is to ‘debug’ later
- My cosc lecturer
If you just code it right from the start, there won’t be exceptions, and handling them won’t be necessary!
A try-except that is expected to be used in non-exceptional situations is a bug. In C, an inappropriate list access will crash your program with a segfault. In python, it generates an IndexError. In C, it’s considered a bug, in python, well, you can catch and ignore it. DON’T. Make sure you aren’t providing your code with bad data internally intentionally.
- Use exception handling when it an error is unexpected but recoverable.
- Don’t use exception handling on internal, recoverable things.
- Raise exceptions wen something unrecoverable happens and providing more data will help debugging.
So when do you use exceptions and error handling?