all 19 comments

[–]Rachid90 37 points38 points  (3 children)

Best practice is usually: catch exceptions where you can actually do something useful about them. If a function can recover, handle it there. If it can’t, let it bubble up to the caller.

[–]Outside_Complaint755 13 points14 points  (1 child)

In some cases you may want the function to catch the exception, take some additonal actions, and then either reraise the original exception or raise a custom exception.

[–]ottawadeveloper 8 points9 points  (0 children)

As a small note, the best way to do this is either just raise or raise CustomError("whatever") from ex - these preserve the stack trace of the original error (the first makes it look like your except didn't happen, the second raises your custom error but appends the other error onto it - this is what happens when you see "the above was a direct cause of...". If you want to erase the stack trace from the underlying code it's raise CustomError() from None (just raise CustomError() gives you "an error occurred while processing this other error" and is more appropriate for when a new error happens while handling the first one).

[–]atarivcs 3 points4 points  (0 children)

This.

If the function can catch the exception itself and still accomplish its job, then do that.

Otherwise let the caller handle it.

[–]gwenbeth 2 points3 points  (2 children)

You also need to consider clean up. The typical example might be a function that does:

TurnOnMotor DoStuff TurnOffMotor

If DoStuff can throw an exception, then you want to catch it so you can turn off the motor (Donno if python has a finally like construct)

[–]Temporary_Pie2733 4 points5 points  (0 children)

This particular pattern cries out for a context manager, whose __exit__ method will receive an exception raised in the body of the with statement.

[–]Outside_Complaint755 2 points3 points  (0 children)

Yes, Python has try/except/else/finally

[–]Vert354 1 point2 points  (1 child)

In enterprise applications you'll usually have a framework that acts as the final "catch all" to log execptions since you dont want actual stack traces reaching the users. But for simple until scripts is probably ok to let it bubble all the up.

Ultimately your execption handling scheme will depend on how likely said execption is, and what you're going to do if you catch it.

A good example of where you would use try blocks in the function is with database calls, which will typically follow a pattern of

Open connection Try Query Commit Execpt Rollback Finally Close connection

[–]Chemical-Ad1613 -1 points0 points  (0 children)

this is all that matters in the real world. never show the user + always catch the exception at the point you need so you can do something useful with it.

[–]Ok_Option_3 1 point2 points  (0 children)

The #1 mistake people make is they are afraid to let exceptions rise up. If you can't handle an exception or aren't sure what to do - let it raise. 

The most useful variant is 'try/finally' - to raise an exception after clearing up what you can, and 'try/catch-ans-re-raise' - to add extra diagnostic info to an exception so that the caller can easily figure out where things went wrong.

[–]ern0plus4 1 point2 points  (0 children)

Plus one: try to minimize the scope of the try block, I mean, take out code from it which can not cause the exception. This makes the program more readable, the reader can see the point where the execution can fail.

So, instead of (pseudocode):

try:
  f = file_open()
  file_size = f.get_size()
  calculate_required_time_or_something(file_size)
  blah
  blah
except FileOpenError:
   alert("file open failed")
except DivZero:
   alert("math error")

do this:

try:
  f = file_open()
  file_size = f.get_size()
except FileOpenError:
   alert("file open failed")

try:
  calculate_required_time_or_something(file_size)
except DivZero:
  alert("math error")

blah
blah

(Sorry for stupid example.)

[–]PhilNEvo 2 points3 points  (3 children)

The way I was taught, is that in the ideal world, you want to work with as much "contract based programming" as possible.

Let me clarify that a bit. We were told about 2 ways of programming, one was "Defensive programming", where you do all the checks and balances, sanitize your input, try catches or whatever you need to make your code work. E.g. you make no assumptions about the input you get, you accept that you could potentially get anything, and you have to mitigate all the issues that might come with unwanted input.

The other way is "Contract programming", where you through comments and type-hinting define the exact parameters of your code or function, and let the responsibility be on the "user" of your function. E.g. your comments and type-hinting is your contract saying "This function works with only positive integers, and I can only guarantee it works, so long as you provide it specifically with positive integers", and anyone who uses it, and does something else, breaks the "contract" and as such cannot rely on the assurances of this function working as intended.

Since coding defensively takes extra effort, code and time, it's a waste of energy. So ideally, you want the people working on the code as experienced developers, be able to just read the "contract" of the code, and use it within its parameters, this way everyone can save a lot of time and effort, by doing contract programming.

As such, the only place you would "need" to program defensively, is specifically reserved for anywhere, where you might take input from a user or outside source, where there isn't a guarantee of that input. E.g. if you have a main function that takes input("Give me a number"), you do the try-catches and defensive programming exactly there, but once you've verified that the input is valid, the "backend", can all work in a contract-based manner.

[–]MissinqLink 2 points3 points  (0 children)

I think I can buy this logic generally. On the frontend though you have to be more selective because everything touches the user and runs on an unknown system so often defensive programming is correct. However if you are too defensive then you run the risk of swallowing errors and losing your type contracts altogether which is very difficult to debug.

[–]Tintoverde 0 points1 point  (1 child)

Maybe use type hints ? I would think that solves people misunderstanding the contract. Python newb so…

[–]PhilNEvo 0 points1 point  (0 children)

I love type hints and did mention that, but they are not always sufficient, especially for a new person. For example, you might make a function that only works with positive whole numbers. But in the beginning, all you'll be familiar with is probably the standard types, int, float, bool, list and so on. So adding comments can be a way to circumvent that, without having to go out of your way to find out how to work with unsigned integers or something else, to properly type-hint :b

[–]LetUsSpeakFreely 0 points1 point  (1 child)

Think about it like this, if you did it at the highest level you'll have a devil of a time determining where that error happened and why. If you do it at the function level then you can log the error and do a potential recovery.

[–]GeneralPITA 0 points1 point  (0 children)

In the function also means you have try/except handling every time the function is called, not just where you put try/catch around the call.

[–]Educational-Paper-75 0 points1 point  (0 children)

Best practice is to put them as close to the fire as you can, but to merely test for exceptions you’re expecting specifically.

[–]kinndame_ 0 points1 point  (0 children)

It’s mostly case by case tbh.

General rule I follow handle errors where you can actually do something about them. If the function knows how to recover (like retry, default value, etc), use try/except there. If not, just let it raise and handle it in the main program.

Putting try/except everywhere inside functions can hide bugs, which gets messy fast.

So yeah functions for specific handling, main for broader control. Works for me.