Edgar Navasardyan Edgar Navasardyan - 4 months ago 83
Python Question

Django - exception handling best practice and sending customized error message

I am starting to think about appropriate exception handling in my Django app, and my goal is to make it as user-friendly, as possible. By user-friendliness, I imply that the user must always get a detailed clarification as to what exactly went wrong.
Following on this post, the best practice is to


use a JSON response with status 200 for your normal responses and
return an (appropriate!) 4xx/5xx response for errors. These can carry
JSON payload, too, so your server side can add additional details
about the error.


I tried to google by the key words in this answer, by still have more questions than answers in my head.


  1. How do I decide upon which error code - 400 or 500 - to return? I mean, Django has many predefined error types, and how can I implement this mapping between Django exception types and 400-500 error code to make the exception handling blocks as DRY and reusable as possible?

  2. Can the approach with middleware suggested by @Reorx in the post be considered viable ? ( The answer got only one upvote, thus making me reluctant to delve into details and implement it in my project

  3. Most importantly, sometimes I may wish to raise an error related to business logic, rather than incorrect syntax or something standard like null value. For example, if there's no CEO in my legal entity, I might want to prohibit the user from adding a contract. What should be the error status in this case, and how do I throw an error with my detailed explanation of the error for the user?



Let us consider it on a simple view

def test_view (request):

try:
# Some code ....
if my_business_logic_is_violated():
# How do I raise the error
error_msg = "You violated bussiness logic because..."
# How do I pass error_msg
my_response = {'my_field' : value}
except ExpectedError as e:
# what is the most appropriate way to pass both error status and custom message
# How do I list all possible error types here (instead of ExpectedError to make the exception handling block as DRY and reusable as possible
return JsonResponse({'status':'false','message':message}, status=500)

Answer

The status codes are very well defined in the HTTP standard. You can find a very readable list on Wikipedia. Basically the errors in the 4XX range are errors made by the client, i.e. if they request a resource that doesn't exist, etc. The errors in the 5XX range should be returned if an error is encountered server side.

With regards to point number 3, you should pick a 4XX error for the case where a precondition has not been met, for example 428 Precondition Required, but return a 5XX error when a server raises a syntax error.

One of the problems with your example is that no response is returned unless the server raises a specific exception, i.e. when the code executes normally and no exception is raised, neither the message nor the status code is explicitly sent to the client. This can be taken care of via a finally block, to make that part of the code as generic as possible.

As per your example:

def test_view (request):
   try:
       # Some code .... 
       status = 200
       msg = 'Everything is ok.'
       if my_business_logic_is_violated():
           # Here we're handling client side errors, and hence we return
           # status codes in the 4XX range
           status = 428
           msg = 'You violated bussiness logic because a precondition was not met'.
   except SomeException as e:
       # Here, we assume that exceptions raised are because of server
       # errors and hence we return status codes in the 5XX range
       status = 500
       msg = 'Server error, yo'
   finally:
       # Here we return the response to the client, regardless of whether
       # it was created in the try or the except block
       return JsonResponse({'message': msg}, status=status)