finally
block is used if we want to make sure that the post process is done in all cases. There’s no problem if the post process doesn’t raise an error. However, what if it raises an error in finally block? The first error is overwritten in this case. How can we implement it in this case?
First error is overwritten by the second error
Assume that we want to reset state after doing a specific process. We need to reset the state even if it raises an error.
def do_something(value: int):
if value > 0:
raise RuntimeError(f"error from do_something. value: {value}")
print(f"do something. value: {value}")
def reset_state(value: int) -> None:
if value > 0:
raise ValueError("value must be bigger than 0")
print("reset done")
def run_reset1(value: int):
try:
do_something(value)
except RuntimeError as err:
print(err)
finally:
reset_state(value)
It doesn’t raise any error if 0 is passed to run_reset1()
.
run_reset1(0)
# do something. value: 0
# reset done
However, an error is raised from finally block if 1 is passed to the function.
run_reset1(1)
# error from do_something. value: 1
# Traceback (most recent call last):
# File "/workspaces/blogpost-python/src/error_from_finally.py", line 52, in <module>
# run_reset1(1)
# File "/workspaces/blogpost-python/src/error_from_finally.py", line 22, in run_reset1
# reset_state(value)
# File "/workspaces/blogpost-python/src/error_from_finally.py", line 12, in reset_state
# raise ValueError("value must be bigger tha
The error raised from do_something()
is caught and handled properly but the error from reset_state()
is not handled here.
If the caller side uses try-except block, the error is of course caught there.
def catch_error1(value: int):
try:
run_reset1(value)
except Exception as ex:
print(ex)
catch_error1(1)
# error from do_something. value: 1
# value must be bigger than 0
Let’s add additional error that is not caught in run_reset1()
def do_something(value: int):
if value > 5:
raise NameError(f"uncaught error from do_something. value: {value}")
if value > 0:
raise RuntimeError(f"error from do_something. value: {value}")
print(f"do something. value: {value}")
catch_error1(10)
# value must be bigger than 0
What? We wanted NameError
but the first error is overwritten by the second error.
Append the error into an error
It doesn’t work properly if the caller side expects that the first error is raised. Since only one error can be raised, we need to put all the errors into one error class.
class MultiExceptions(Exception):
def __init__(self, errors: List[Exception]) -> None:
self.errors = errors
messages = ", ".join([f"{type(x)}: {str(x)}" for x in errors])
super().__init__(messages)
This custom error can hold multiple errors as a list. We can check whether the desired error is included in this list or not.
We can no longer use finally block in this way. So we need to split the logic into two parts in this way.
def run_reset2(param: int):
errors: List[Exception] = []
try:
do_something(param)
except Exception as err:
errors.append(err)
try:
reset_state(param)
except Exception as err:
errors.append(err)
if len(errors) == 1:
raise errors[0]
if len(errors) > 1:
raise MultiExceptions(errors)
print("completed")
Add an error to the errors list in except block. The process do the job without error if the length of the array is 0.
If we need to check the specific error, we can for example implement it in this way.
def catch_error2(value: int):
try:
run_reset2(value)
except MultiExceptions as ex:
print(f"ERRORS: {ex}")
if RuntimeError in ex.errors:
print("RuntimeError is included.")
for error in ex.errors:
if isinstance(error, RuntimeError):
print(f"RuntimeError: {error}")
catch_error2(0)
# do something. value: 0
# reset done
# completed
catch_error2(1)
# ERRORS: <class 'RuntimeError'>: error from do_something. value: 1, <class 'ValueError'>: value must be bigger than 0
# RuntimeError: error from do_something. value: 1
Comments