Django Tips: Recovering Gracefully From ORM Errors
Django views are the glue between your users and the underlying database. When a user visits an url Django maps the url with a view. And most of the times the view is also responsible for fetching some data from the database.
Consider the following example. There is a model named Workshop which among the others has a slug field:
"""
models.py
"""
from django.db import models
class Workshop(models.Model):
"""
... some other field here
"""
slug = models.SlugField(max_length=20)
"""
... some other field here
"""
def __str__(self):
return f'{self.title}'
You may want to use that slug for fetching the appropriate entity from the database when the user visits someurl/slugname/. That means you will have an url declaration like this:
"""
urls.py of your Django app
"""
from django.urls import path
from .views import workshop_detail
urlpatterns = [
path('someurl/<slug:slug>/', workshop_detail)
]
At this point you're ready to create a Django view for displaying the model detail. I'll use a function view for keeping things easy. A naive implementation for the view could be the following:
"""
views.py of your Django app
"""
from django.shortcuts import render
from .models import Workshop
def workshop_detail(request, slug):
workshop = Workshop.objects.get(slug=slug)
context = { 'workshop': workshop }
return render(request,
f'academy/workshop_detail.html',
context=context)
The view should fetch an instance of the model matching the given slug. Django has a fantastic ORM, and we can use a get()
method for fetching the instance:
workshop = Workshop.objects.get(slug=slug)
Next up we build a context object for holding our model and maybe some other stuff for the actual template:
context = { 'workshop': workshop }
Finally we can render the template alongside with the context:
return render(request,
f'yourapp/workshop_detail.html',
context=context)
At this point we're ready to smoke test our Django app in the browser. But first let's talk a moment about get()
.
Recovering Gracefully From ORM Errors: objects.get()
The get()
method returns the matching model instance as long as said instance exists. If you run your Django app straight away without creating any entity inside the database, well your view will fail miserably. If the user goes to /someurl/py/ he/she is welcomed with a scary error: Model matching query does not exist.
get()
in fact raises a DoesNotExists exception when it can't find any model matching the query. That's reasonable! But from a UX perspective it is a bit rough four our users.
So what's the solution? Luckily we can catch the exception with try/except for gracefully handling the error. First make sure to import ObjectDoesNotExist in your view:
"""
views.py of your Django app
"""
from django.shortcuts import render
from .models import Workshop
from django.core.exceptions import ObjectDoesNotExist
"""
actual view here
"""
Next up we wrap get()
and render in a try/except block:
"""
views.py of your Django app
"""
from django.shortcuts import render
from .models import Workshop
from django.core.exceptions import ObjectDoesNotExist
def workshop_detail(request, slug):
try:
workshop = Workshop.objects.get(slug=slug)
context = { 'workshop': workshop }
return render(request,
f'yourapp/workshop_detail.html',
context=context)
except ObjectDoesNotExist:
"""
do something here
"""
pass
The above code tries to fetch an entity from the database and then renders a template to the user.
If Workshop.objects.get(slug=slug)
fails, except catches the error. What to do in the except is up to you. Maybe logging the error? More important though, we can continue executing the code outside try/except and show a courtesy message for our users.
For example, you can import HttpResponseNotFound in your view and render a generic error:
"""
views.py of your Django app
"""
from django.shortcuts import render
from .models import Workshop
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponseNotFound
def workshop_detail(request, slug):
try:
workshop = Workshop.objects.get(slug=slug)
context = { 'workshop': workshop }
return render(request,
f'yourapp/workshop_detail.html',
context=context)
except ObjectDoesNotExist:
"""
do something here
"""
pass
return HttpResponseNotFound('Oops! Not found!')
Even better you can show a nice 404 page! But what if I tell you there is a cleaner way for dealing with this kind of situation?
Going fancy with get_object_or_404
You don't want to import ObjectDoesNotExist, HttpResponseNotFound, and wrap everything in try/except all over the place. Django offers a cleaner way for dealing with errors coming from the ORM: get_object_or_404
.
It does two things:
-
calls
Modelname.object.get()
with the desired arguments -
raises Http404 if no instance of the model is found
This is convenient because when using HttpResponseNotFound you should also provide some HTML for the user. If we just raise Http404 Django catches the exception and returns a 404 page for us.
Also, when DEBUG
is True you'll see a "page not found" error. In production with DEBUG=False
the user will see a generic 404 page. So back to our code, we can refactor like so:
"""
views.py of your Django app
"""
from django.shortcuts import render, get_object_or_404
from .models import Workshop
def workshop_detail(request, slug):
workshop = get_object_or_404(Workshop, slug=slug)
context = { 'workshop': workshop }
return render(request,
f'yourapp/workshop_detail.html',
context=context)
Look how cleaner is that! We get rid of ObjectDoesNotExist, HttpResponseNotFound, and try/except. And the result is even better!
Wrapping up
try/except is your friend whenever you want to recover from a fatal error in Python. It is also useful for dealing with Django ORM methods that can potentially raise exceptions.
The get()
method of the ORM raises when it cannot find the desired model in the database. That's a good reason for wrapping the call within a safe block. But cluttering your code with try/except is not always the right thing to do.
Luckily Django has get_object_or_404
which wraps get()
in a try/except and raises for us an Http404. Django is then able to catch the Http404 and showing a Not found page to our users.
In this post you learned how to make your Django function view more robust with try/except and get_object_or_404
, the latter being more convenient and idiomatic.
Thanks for reading and stay tuned on this blog!