An application needs to behave differently depending on an exception. If we need to handle an app-specific or business-specific error, we need to define our own exception.
If not using a custom exception, the code will look like the following.
try:
raise Exception("Database is not ready")
except Exception as err:
if "Database is not ready" in str(err):
# Do something special here
print("wait for the initilization of the Database")
else:
print("unknown error")
This is not a good design. The message might change in the future and it makes a bug. Let’s learn how to create a custom exception.
If you want to have the complete code, you can check it in my GitHub repository.
Define a custom exception that can take an error message
The easiest way to define a custom exception is the following.
class CustomException(Exception):
pass
It just extends the Exception and gives a different name to it. The previous code can be written in the following way.
try:
raise CustomException("Database is not ready")
except CustomException as err:
print(f"error: {str(err)}")
print("wait for the initilization of the Database")
except Exception as err:
print("unknown error")
# error: Database is not ready
# wait for the initilization of the Database
An except
keyword is added but if-else
is removed. Even if the error message changes, the error handling still works.
If the thrown error is not CustomException
, it will be handled in Exception error handling.
Fixed error message
If we want the same error message for all the places where the exception is used, specify the error message in the constructor.
class MyExceptionWithFixedMsg(Exception):
def __init__(self):
super().__init__("An error occurred for some reason")
try:
raise MyExceptionWithFixedMsg()
except MyExceptionWithFixedMsg as err:
print(str(err))
# An error occurred for some reason
When the error is thrown, the error message is always this one.
Specify some parameters
If some parameters are needed, add them to __init__
function and assign them to self.variable_name
.
class MyExceptionWithParameters(Exception):
def __init__(self, error_code):
self.error_code = error_code
super().__init__(f"MyExceptionWithParameters error message. error_code: {error_code}")
try:
raise MyExceptionWithParameters(123)
except MyExceptionWithParameters as err:
print(f"error_code: {err.error_code}")
print(f"str(err): {str(err)}")
# error_code: 123
# str(err): MyExceptionWithParameters error message. error_code: 123
Extend a custom exception
A custom exception can be extended for example when you want to add parameters to the base class.
class MyExtendedException(MyExceptionWithFixedMsg):
def __init__(self, error_code):
self.error_code = error_code
super().__init__()
try:
raise MyExtendedException(123)
except MyExtendedException as err:
print(f"error_code: {err.error_code}")
print(f"str(err): {str(err)}")
# error_code: 123
# str(err): An error occurred for some reason
Note that super().__init__()
needs to be called, otherwise, it doesn’t make sense to extend the class. If it doesn’t call it, the result will be like this.
class MyExtendedException(MyExceptionWithFixedMsg):
def __init__(self, error_code):
self.error_code = error_code
try:
raise MyExtendedException(123)
except MyExtendedException as err:
print(f"error_code: {err.error_code}")
print(f"str(err): {str(err)}")
# error_code: 123
# str(err): 123
Overwrite the error message
By the way, it can’t pass an error message to the super().__init__()
because MyExceptionWithFixedMsg
class doesn’t have the parameter. However, there might be some cases where we want to update the error message for the extended class. Define __str__()
method in this case.
class MyExtendedException(MyExceptionWithFixedMsg):
def __init__(self, error_code):
self.error_code = error_code
super().__init__()
def __str__(self):
return f"MyExtendedException(error_code: {self.error_code})"
try:
raise MyExtendedException(error_code=123)
except MyExtendedException as err:
print(f"error_code: {err.error_code}")
print(f"str(err): {str(err)}")
# error_code: 123
# str(err): MyExtendedException(error_code: 123)
__str__()
is called when the object needs to be converted to string. So, the error message can be overwritten.
Named parameters
It might be better to have named parameters to make it readable. In this case, add an asterisk to the parameter. The parameters defined after the asterisk can be specified with the name.
class TooColdException(Exception):
def __init__(
self,
*,
error_code=None,
extra_msg=None,
):
self.error_code = error_code
self.extra_msg = extra_msg
if error_code == 1:
action = "Close all windows."
elif error_code == 2:
action = "Turn the heater on."
else:
action = "Wait for Summer."
super().__init__(
f"Too Cold Exception occurred.\n"
f"Error code: {error_code}\n"
f"Action: {action}\n"
f"Extra_msg: {extra_msg}"
)
try:
raise TooColdException(error_code=123, extra_msg="hello")
except TooColdException as err:
print(f"error_code: {err.error_code}, extra_msg: {err.extra_msg}")
print(f"str(err): {str(err)}")
# error_code: 123, extra_msg: hello
# str(err): Too Cold Exception occurred.
# Error code: 123
# Action: Wait for Summer.
# Extra_msg: hello
Handling error depending on the exception type
If a function can throw many types of exceptions, we need to handle them correctly. Basically, the error should be handled depending on the exception type but if the exception class has an error code, we can also handle them.
errors = [
MyExceptionWithFixedMsg(),
MyExtendedException(55),
Exception("Normal error"),
TooColdException(),
TooColdException(error_code=1),
TooColdException(
error_code=2, extra_msg="It is 22 degrees in the room though."
),
TooColdException(error_code=3),
]
for error in errors:
try:
raise error
except TooColdException as err:
print(f"TooColdException: {str(err)}")
if err.error_code == 1:
print("-----> I will close all windows later.")
elif err.error_code == 2:
print("-----> I will turn the heater on in 30 minutes.")
else:
print(
f"-----> I don't know what to do for error code [{err.error_code}]"
)
except MyExtendedException as err:
print(f"MyExtendedException: {str(err)}, error_code: {err.error_code}")
except MyExceptionWithFixedMsg as err:
print(f"MyExceptionWithFixedMsg: {str(err)}")
except Exception as err:
print(f"Exception: {str(err)}")
print()
# MyExceptionWithFixedMsg: An error occurred for some reason
# MyExtendedException: An error occurred for some reason, error_code: 55
# Exception: Normal error
# TooColdException: Too Cold Exception occurred.
# Error code: None
# Action: Wait for Summer.
# Extra_msg: None
# -----> I don't know what to do for error code [None]
# TooColdException: Too Cold Exception occurred.
# Error code: 1
# Action: Close all windows.
# Extra_msg: None
# -----> I will close all windows later.
# TooColdException: Too Cold Exception occurred.
# Error code: 2
# Action: Turn the heater on.
# Extra_msg: It is 22 degrees in the room though.
# -----> I will turn the heater on in 30 minutes.
# TooColdException: Too Cold Exception occurred.
# Error code: 3
# Action: Wait for Summer.
# Extra_msg: None
# -----> I don't know what to do for error code [3]
It works pretty well.
raise an Exception from another exception
An Exception can be thrown from another exception. It can wrap the original exception and generate a new exception.
try:
try:
raise MyExceptionWithParameters(999)
except Exception as err:
raise TooColdException(
error_code=12,
extra_msg="It's cold.",
) from err # raise an exception with from keyword here
except Exception as error:
print(isinstance(error, Exception)) # True
print(isinstance(error, MyExceptionWithParameters)) # False
print(isinstance(error, TooColdException)) # True
Since the exception is wrapped, it is not an instance of the original exception. This from
keyword can be useful if the exception is caused by another exception and we want to know the root cause. The wrapped error message can be read via __cause__
.
try:
try:
raise MyExceptionWithParameters(999)
except Exception as err:
raise TooColdException(
error_code=12,
extra_msg="It's cold.",
) from err
except TooColdException as error:
print(f"error code: {error.error_code}")
print(f"extra msg: {error.extra_msg}")
print(f"str(error) >>> {str(error)}")
print(f"str(error.__cause__) >>> {str(error.__cause__)}")
# error code: 12
# extra msg: It's cold.
# str(error) >>> Too Cold Exception occurred.
# Error code: 12
# Action: Wait for Summer.
# Extra_msg: It's cold.
# str(error.__cause__) >>> MyExceptionWithParameters error message. error_code: 999
error.__cause__
is None if there is no from
keyword there.
Comments