1

Let's assume that we have the following flow:

def flow(input_val: Any) -> Any: result1 = function1(input_val) result2 = function2(result1) result3 = function3(result2) return result3 

And let's say that I want to be able to catch exceptions for each of these three steps:

def flow(input_val: Any) -> Any: try: result1 = function1(input_val) except Exception as ex: print("Function 1 error: "+str(ex)) try: result2 = function2(result1) except Exception as ex: print("Function 2 error: "+str(ex)) try: result3 = function3(result2) except Exception as ex: print("Function 3 error: "+str(ex)) return result3 

This doesn't look like the best way of handling exceptions in a flow like this, because if the first exception is caught, then result1 won't be defined. Also, if the third exception is caught, there won't be anything to return.

What's the best way of handling these situations?

9
  • 1
    The problem is that you are effectively ignoring the exception (you catch it, but then just log it and move on as if it never happened). You either need to provide a definition of result1 so your code can continue, return early, or raise an exception (the same one you just caught or a new one) to prevent the rest of flow from trying to execute without result1. Commented Feb 24, 2023 at 17:09
  • Just because an exception could be or is raised doesn't mean you must catch it. The whole point of exception handling is to let exceptions you can't do anything about bubble up to someone who can do something with it. Commented Feb 24, 2023 at 17:11
  • if function1 raises, do you really want to attempt function2? Commented Feb 24, 2023 at 17:12
  • 1
    Perhaps not orthodoxically, I'm trying to use the try/except block to figure out which function in my flow failed. Commented Feb 24, 2023 at 17:12
  • 2
    There are mutliple use cases. You could have a single outer try/except that logs the exception if that's all you need. You could assign default values to the variables for cleanup in a finally clause. If you need to change your code behavior based on exception... then maybe these functions should have been returning an error code instead of raising exceptions. The is an argument that exceptions should only be exceptional - exactly because of what you see here. When you need detailed control of error handling, they are a pain. Commented Feb 24, 2023 at 17:21

5 Answers 5

3

You could write the functions sequentially in a "happy path" mindset and set a context variable (before each step) that you then use in your error handling logic.

def flow(input_val: Any) -> Any: try: context = "Function 1" result1 = function1(input_val) context = "Function 2" result2 = function2(result1) context = "Function 3" result3 = function3(result2) except Exception as ex: print(context," error: "+str(ex)) return None return result3 

This gives you the same level of control but keeps the normal sequence of code together instead of having the intended logic drowned in error handling code.

Sign up to request clarification or add additional context in comments.

Comments

2

If you don't catch an exception, flow will immediately terminate and the caller will have to catch the exception or terminate itself. This implies that if you do catch the exception, you need to do something more than just log that it happened. If function1 raises an exception, result1 is not set, so you cannot proceed unless you provide a value yourself. Otherwise, you should return immediately or raise an exception (the same one you just caught, or a new one).

# Option 1 def flow(input_val: Any) -> Any: try: result1 = function1(input_val) except Exception as ex: print("Function 1 error: "+str(ex)) result1 = 5 ... 

# Option 2 def flow(input_val: Any) -> Any: try: result1 = function1(input_val) except Exception as ex: print("Function 1 error: "+str(ex)) return ... 

# Option 3a def flow(input_val: Any) -> Any: try: result1 = function1(input_val) except Exception as ex: print("Function 1 error: "+str(ex)) raise # Give someone else a crack at ex ... 

# Option 3b def flow(input_val: Any) -> Any: try: result1 = function1(input_val) except Exception as ex: print("Function 1 error: "+str(ex)) raise SomeOtherException("Function 1 failed") ... 

2 Comments

Option 2 sounds interesting: the flow is stopped with a return call that outputs nothing. But what about the rest of the flow? If the first try is successful, is result1 available to the next try-except block to use?
Yes; try blocks do not start a new scope. Assuming function1 does not raise, its returned value is assigned to result1 and result1 remains in scope as you execute the next try block.
1

As I said in the comment, try-except blocks

 def flow(input_val: Any) -> Any: input_val = <insert some value> result1 = None try: result1 = function1(input_val) except Exception as ex: print("error: " + str(ex)) return None, None, None result2 = None try: result2 = function2(result1) except Exception as ex: print("error: " + str(ex)) return result1, None, None result3 = None try: result3 = function3(result2) except Exception as ex: print("error: " + str(ex)) return result1, result2, None return result1, result2, result3 

Comments

1

You could use a loop, if all your sequentially called functions take the last result as argument:

def function1(arg): argValue = arg + 1 return argValue function2, function3 = function1,function1 def flow(input_val): result = [input_val] for fn in (function1, function2, function3): try: result.append(fn(result[-1])) except: print("Error@%s" % fn.__name__) return return result # or result[-1] in order to return last result only! print(flow(0)) 

Out:

[0, 1, 2, 3] 

Comments

1

Based on your comment:

I'm trying to use the try/except block to figure out which function in my flow failed.

I believe what you are after is to catch the exception and use its traceback to get the "name" of the offending function. This seems like a much more generic approach to me:

import traceback def function1(i): return i def function2(i): return i/0 def function3(i): return i def flow(input_val): try: return function3(function2(function1(input_val))) except Exception as e: tb = traceback.extract_tb(e.__traceback__) print(f"Failure from: \"{tb[-1][2]}\"") ## ------------------- ## Note: still prints None as you don't re-cast e ## ------------------- print(flow(10)) ## ------------------- 

This will result in:

Failure from: "function2" None 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.