Django - best practices for handling exceptions and sending custom error messages

I'm starting to think about appropriate exception handling in my Django application, and my goal is to make it as user-friendly as possible. For the convenience of the user, I mean that the user should always get a detailed explanation of exactly what went wrong. Following this post , best practice is

use a JSON response with a status of 200 for your regular answers and return a response (corresponding!) 4xx / 5xx for errors. They can carry JSON, so your server side can add additional error information.

I tried Google with the keywords in this answer, I have even more questions than the answers in my head.

  • How can I decide which error code - 400 or 500 - to return? I mean, Django has a lot of predefined error types and how can I implement this mapping between Django exception types and error code 400-500 to make exception handling blocks as DRY and reused as much as possible?
  • Could the middleware approach proposed by @Reorx in the post be considered viable? (The answer received only one contribution, which made me reluctant to delve into the details and implement it in my project.
  • Most importantly, sometimes I may need an error related to business logic, and not with incorrect syntax or something standard, for example, with a zero value. For example, if my legal entity does not have a CEO, I can prevent the user from adding a contract. What should be the error status in this case and how can I make a mistake with my detailed explanation of the error for the user?

Consider this in a simple way.

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) 
+8
json python ajax django
source share
2 answers

First of all, you should think about what errors you want to identify:

  • Typically, 4xx errors (client-side errors) are uncovered, so the user can correct the request.

  • On the other hand, 5xx errors (Errors that are related to the server side) are usually provided only without information. In my opinion, for those with whom you should use tools such as Sentry , track and fix these errors that may have security issues built into them.

With that in mind, in my opinion, for the correct Ajax request, you must return a status code, and then some json, in order to understand what happened, like a message and explanation (when applicable).

If your goal is to use ajax to send information, I suggest setting form for what you want. Thus, you easily go through the verification process. I guess the point is in this example.

First - Is the query correct?

 def test_view(request): message = None explanation = None status_code = 500 # First, is the request correct? if request.is_ajax() and request.method == "POST": .... else: status_code = 400 message = "The request is not valid." # You should log this error because this usually means your front end has a bug. # do you whant to explain anything? explanation = "The server could not accept your request because it was not valid. Please try again and if the error keeps happening get in contact with us." return JsonResponse({'message':message,'explanation':explanation}, status=status_code) 

Second - Are there any errors in the form?

 form = TestForm(request.POST) if form.is_valid(): ... else: message = "The form has errors" explanation = form.errors.as_data() # Also incorrect request but this time the only flag for you should be that maybe JavaScript validation can be used. status_code = 400 

You can even get the error field by field so that you can better represent it in the form itself.

Third . Let me handle the request

  try: test_method(form.cleaned_data) except `PermissionError` as e: status_code= 403 message= "Your account doesn't have permissions to go so far!" except `Conflict` as e: status_code= 409 message= "Other user is working in the same information, he got there first" .... else: status_code= 201 message= "Object created with success!" 

Different codes may be required depending on the exceptions you have identified. Go to Wikipedia and check the list. Do not forget that the answer is also different in code. If you add something to the database, you must return 201 . If you just received the information, you were looking for a GET request.

Answering questions

  • Django exceptions will return 500 errors if they are not handled, because if you do not know that an exception will occur, then this is an error on the server. With the exception of 404 and entry requirements, I would have made try catch blocks for everything. (For 404, you can raise it, and if you do @login_required or the permission that is required, django will respond with the appropriate code without doing anything).

  • I do not fully agree with this approach. As you said, the errors must be explicit, so you should always know what should happen, and how to explain it, and make it dependent on the operation being performed.

  • I would say 400 error for this. This is a bad request, you just need to explain why the error code for you and for your js code is so easily matched.

  • (example provided). In text_view you should have test_method , as in the third example.

The testing method should have the following structure:

 def test_method(validated_data): try: my_business_logic_is_violated(): catch BusinessLogicViolation: raise else: ... #your code 

In my example:

  try: test_method(form.cleaned_data) except `BusinessLogicViolation` as e: status_code= 400 message= "You violated the business logic" explanation = e.explanation ... 

I considered the violation of business logic to be a client’s mistake, because if something is necessary before this request, the client should know about it and ask the user to do this first. (From Error Definition ):

Status code 400 (invalid request) indicates that the server cannot or will not process the request due to what is perceived as a client error (for example, invalid request syntax, invalid request
creating messages or misleading request routing).

By the way, see Python Docs on Custom Exceptions , so you can give relevant error messages. The idea behind this example is that you throw a BusinessLogicViolation exception with another message in my_business_logic_is_violated() according to where it was generated.

+8
source share

Status codes are very well defined in the HTTP standard. You can find a very readable list on Wikipedia . Basically, errors in the 4XX range are errors made by the client, that is, if they request a resource that does not exist, etc. Errors in the 5XX range should be returned if the error occurs on the server side.

For point 3, you should choose a 4XX error for the case when the precondition is not met, for example 428 Precondition Required , but return a 5XX error when the server causes a syntax error.

One of the problems in your example is that the response is not returned if the server does not throw a specific exception, that is, when the code runs normally and the exception does not occur, neither the message nor the status code are explicitly sent to the client. This can be taken care of through the finally block to make this part of the code as general as possible.

According to 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) 

However, as stated in the comments, it would be wiser to do both checks the same way, i.e. through exceptions, for example:

 def test_view (request): try: # Some code .... status = 200 msg = 'Everything is ok.' if my_business_logic_is_violated(): raise MyPreconditionException() except MyPreconditionException as e: # Here we're handling client side errors, and hence we return # status codes in the 4XX range status = 428 msg = 'Precondition not met.' except MyServerException 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) 
+2
source share

All Articles