Django’s 404 handler is a surprisingly flexible beast, capable of much more than just displaying a generic "Page Not Found."

Let’s watch a 404 happen. Imagine this views.py:

from django.http import HttpResponseNotFound

def my_view(request):
    if not some_condition_is_met:
        return HttpResponseNotFound("This specific resource doesn't exist right now.")
    return HttpResponse("This resource exists!")

And a corresponding urls.py:

from django.urls import path
from . import views

urlpatterns = [
    path('my-resource/', views.my_view, name='my_resource'),
]

When a request comes in for /my-resource/ and some_condition_is_met is False, the browser will get a 404 response. The content will be exactly "This specific resource doesn’t exist right now." This is the simplest form: returning HttpResponseNotFound directly from a view.

But Django’s real power lies in its configurable 404 handler. Instead of returning HttpResponseNotFound directly, you can let Django’s URL resolver do the work. When the resolver can’t find a matching URL pattern for an incoming request, it triggers a 404. By default, this shows a simple debug page (if DEBUG = True) or a generic "Not Found" page (if DEBUG = False). You can customize this behavior.

Here’s how to set up a custom 404 view. First, create a view function, typically in a views.py file in one of your apps, or even in a dedicated core app:

# core/views.py
from django.shortcuts import render

def custom_page_not_found_view(request, exception):
    return render(request, '404.html', {}, status=404)

This view takes the request object and an exception (which contains details about the missing URL). It then renders a template named 404.html.

Next, tell Django to use this view in your project’s urls.py:

# project/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    # ... other app urls
]

# This line MUST be at the root urls.py, not in an app's urls.py
handler404 = 'core.views.custom_page_not_found_view'

Now, when Django can’t find a URL pattern, it will execute core.views.custom_page_not_found_view.

You’ll also need to create the 404.html template. Place it in your project’s templates directory, or within an app’s templates directory that’s included in settings.TEMPLATES['DIRS'].


{# templates/404.html #}


{% extends "base.html" %}



{% block title %}Page Not Found{% endblock %}



{% block content %}

    <h1>Oops! That page seems to have wandered off.</h1>
    <p>We couldn't find the page you were looking for. Maybe try heading back to the <a href="/">homepage</a>?</p>
    <p><em>Error details: The URL requested was not found.</em></p>

{% endblock %}

This template can be as simple or as complex as you need. It can include navigation, links, or even helpful debugging information (though be careful about exposing too much sensitive info in production).

The exception argument passed to your custom 404 view is an instance of django.http.Http404. It doesn’t carry much useful information by default, but it’s the standard way for Django to signal a missing resource.

The most surprising thing about Django’s 404 handling is that you can also trigger a 404 within a view that successfully matched a URL pattern. This is useful when an object identified by a URL parameter doesn’t actually exist. For example, if you have a URL like /users/<int:user_id>/, and a user_id is provided that doesn’t correspond to an actual user in your database, you should raise Http404 instead of returning a 400 or 500 error.

# users/views.py
from django.http import Http404
from django.shortcuts import render
from .models import User

def user_detail_view(request, user_id):
    try:
        user = User.objects.get(pk=user_id)
    except User.DoesNotExist:
        raise Http404("User does not exist") # This will trigger the handler404

    return render(request, 'users/detail.html', {'user': user})

When User.DoesNotExist is caught, raise Http404(...) is executed. This interrupts the normal flow and passes control to your configured handler404. The string passed to Http404 isn’t directly used by the default handler, but it’s good practice to include a descriptive message.

The exception object in custom_page_not_found_view(request, exception) is actually an instance of django.core.exceptions.PageNotFound. While the exception type is Http404 when raised manually, Django’s internal handler catches it and passes PageNotFound to your view. This object has a request_path attribute containing the URL that was not found.

The key takeaway is that handler404 is your global fallback for any unresolvable URL, and raising Http404 within a view is how you signal that a specific resource, even if its URL pattern matches, cannot be found.

Once you’ve got your 404s handled cleanly, your next challenge will be managing other HTTP status codes, like 500 Internal Server Errors, and creating custom error pages for them.

Want structured learning?

Take the full Django course →