3

I'm in the midst of building a Django app and am hoping to get some advice on the proper way to handle errors and bugs in my code.

Here's a common situation exemplary of the problems I have: a user purchases a product. To process the purchase, my views need to perform a number of actions:

  1. First, the view should create a User object in the database.
  2. If that is successful, the view should create an Order object and assign it to the newly-created User.
  3. If that is successful, my code should create a Product object and add it to the newly created Order.

This is all well and good when no errors occur - but I find that the occasional error is inevitable in my code, and I want my app to deal with errors gracefully, rather than crashing outright. For example, if, for any reason, an Order object cannot be created, the view should show the user an error and remove the User object that was previously created. And, it should throw a graceful error message rather than crashing outright and serving the user a Http 500 error.

The only way I can think of to do this is an extremely complex series of nested try / except clauses, like below. But designing my code this way is extremely messy and time-consuming, and it doesn't feel like the right way to do things. I know there must be a better way to design for proper error handling in Django and Python, but I'm not quite sure what it is.

I would greatly appreciate any advice on how to better structure my code in this situation.

Example code:

try:

    # Create a new user
    u = User(email='[email protected]')
    u.save()

    try:

        # Create a new order
        o = Order(user=u, name='Order name')
        o.save()

        try:

            # Create a new product
            p = Product(order=o, name='Product name')
            p.save()

        # If a product cannot be created, print an error message and try deleting the user and order that were previously created
        except:

            messages.add_message(request, messages.ERROR, 'Product could not be created')

            # If deleting the order doesn't work for any reason (for example, o.save() didn't properly save the user), 'pass' to ensure my application doesn't crash
            try:
                o.delete()

            # I use these 'except: pass' clauses to ensure that if an error occurs, my app doesn't serve a Http 500 error and instead shows the user a graceful error
            except:
                pass

            # If deleting the user doesn't work for any reason (for example, u.save() didn't properly save the user), 'pass' to ensure my application doesn't crash
            try:
                u.delete()
            except:
                pass

    # If an order cannot be created, print an error message and try deleting the user that was previously created
    except:
        messages.add_message(request, messages.ERROR, 'Order could not be created')

        # If deleting the user doesn't work for any reason (for example, u.save() didn't properly save the user), 'pass' to ensure my application doesn't crash
        try:
            u.delete()
        except:
            pass

# If the user cannot be created, throw an error
except:
    messages.add_message(request, messages.ERROR, 'User could not be created')

2 Answers 2

4

I would recommend using transaction.atomic block which should enclose your models creation like so (link from django docs):

try:
    with transaction.atomic():
        create_your_objects()
except IntegrityError:
    handle_exception()

In this way any change done inside context manager will be rollbacked automatically in case of any occurred problem.

P.S. Actually this is how django deals by default with each view but for your case as you expect it to fail then you can get rid of 500 error and still get clean database in case of occurred problem without need to drop each and every created object.

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

1 Comment

This is exactly what I was looking for. Thank you for your help!
-1

How about this? Assign each of the things you're trying to create to None. If instantiating them (calling the constructor) throws an exception, they will remain None. On the other hand, if they are instantiated correctly, but they are not saved correctly, then their primary key will be None. Thus:

u = None
o = None
p = None

try:
    # Create a new user
    u = User(email='[email protected]')
    u.save()

    # Create a new order
    o = Order(user=u, name='Order name')
    o.save()

    # Create a new product
    p = Product(order=o, name='Product name')
    p.save()

except:
    if u == None or u.pk == None:
        messages.add_message(request, messages.ERROR, 'User could not be created')

    else if o == None or o.pk == None:
        messages.add_message(request, messages.ERROR, 'Order could not be created')

    else if p == None or p.pk == None:
        messages.add_message(request, messages.ERROR, 'Product could not be created')

You can improve this further and make it more contact by looping over a list of u, o, and p, rather than repeating code the way I did, but this should give you the general idea.

As an aside: Functional programming offers some insights into this, since pure functional programming prohibits exception throwing. Such languages have Maybe monads (the name can vary), which is basically a wrapper class which has two possible values: the object itself, or None, and it has a method get_or_else which takes a parameter foo, and returns the object it stores if it exists, or foo if its null. You could implement this rather easily in Python, but I believe it's considered unpythonic.

4 Comments

Thanks for your response! However, this answer doesn't solve the problem; if, for example, the order creation line fails, there is no code in your example that ensures the newly=created User object is also deleted.
@Sam that seems easy to modify- since a product can only be created is there was an order, and an order only created if there is a user, at each of the if checks, delete all dependent objects. The other option is, instead of a general except, you can check for specific exceptions, since difference exceptions are thrown by save and the constructors.
Thanks for the thoughts @ubadub! Will be keeping the above response (re: transaction.atomic) as the approved answer, but this could work as well as an alternate solution.
@Sam I read that solution just now and you are correct, that is the superior solution. I was not aware of this feature in Django, it's good to know

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.