Back to Scaling Python Applications guides

Django Error Handling Patterns

Stanley Ulili
Updated on February 27, 2025

Error handling is important for developing robust, secure, and maintainable Django applications. Without proper error-handling mechanisms, web applications can crash unexpectedly, expose sensitive information, or present confusing error messages to users.

This guide explores the most effective error-handling patterns for your Django projects.

Understanding errors in Django

Errors in Django applications generally fall into three main categories:

Operational errors (expected failures)

These errors occur during regular operations due to external factors rather than bugs in the code. They're expected and should be handled gracefully:

  • A user requests a non-existent resource (resulting in a 404 Not Found)
  • Form validation fails due to invalid input (resulting in a 400 Bad Request)
  • Database connections fail due to network issues (resulting in a 500 Internal Server Error)
  • External API requests timeout or return errors

Programmer errors

These errors result from mistakes in the codebase and typically require fixing rather than runtime handling:

  • Attempting to call methods on None objects (AttributeError)
  • Using incorrect field names in querysets (FieldError)
  • Incorrect type conversions (TypeError)
  • Logic errors in view functions that cause unintended behavior

System errors

These errors occur at the operating system or hardware level and can disrupt application functionality:

  • File system errors when reading or writing static files
  • Database connection failures due to server issues
  • Memory exhaustion causing performance degradation
  • Hardware failures affecting application responsiveness

Now that you understand Django's different types of errors, let's explore effective handling strategies.

Handling errors in Django views

Django provides several approaches for handling errors in views, each with its own benefits and trade-offs. Understanding the correct error handling pattern for your specific use case is critical for building reliable Django applications.

In Django, errors in views occur regularly during request processing. If not properly handled, these errors can crash your application, expose sensitive information, or present confusing messages to users. Implementing proper view-level error handling is your first line of defense.

Function-based views with try/except

The most straightforward approach to error handling in function-based views involves using Python's native try/except blocks to catch and handle specific exceptions:

 
from django.http import JsonResponse
from .models import Book

def book_detail(request, book_id):
    try:
        book = Book.objects.get(id=book_id)
        return JsonResponse({
            'title': book.title,
            'author': book.author,
            'published_date': book.published_date
        })
    except Book.DoesNotExist:
        return JsonResponse({'error': 'Book not found'}, status=404)
    except Exception as e:
        # Log the error here
        return JsonResponse({'error': 'An unexpected error occurred'}, status=500)

In this pattern, you explicitly catch specific exceptions you expect (Book.DoesNotExist) and handle them appropriately. A catch-all Exception handler is a safety net for unexpected errors.

While this approach works, it leads to repetitive error handling code across multiple views. Each view needs its own try/except logic, making your codebase harder to maintain as it grows.

Using Django's get_object_or_404 shortcut

Django provides built-in shortcuts that handle common error scenarios without explicit try/except blocks. The get_object_or_404 function is an excellent example that cleanly handles cases when a requested object doesn't exist:

 
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from .models import Book

def book_detail(request, book_id):
    book = get_object_or_404(Book, id=book_id)
    return JsonResponse({
        'title': book.title,
        'author': book.author,
        'published_date': book.published_date
    })

This shortcut automatically raises an Http404 exception when the requested object isn't found. Django's middleware then converts this exception into a proper 404 response without any additional code in your view.

Similar shortcuts include get_list_or_404 for queryset results and require_http_methods for enforcing specific HTTP methods on views.

Class-based views with exception handling

Class-based views (CBVs) offer a more structured approach to error handling through method overriding:

 
from django.http import JsonResponse
from django.views import View
from .models import Book

class BookDetailView(View):
    def get(self, request, book_id):
        try:
            book = Book.objects.get(id=book_id)
            return JsonResponse({
                'title': book.title,
                'author': book.author,
                'published_date': book.published_date
            })
        except Book.DoesNotExist:
            return JsonResponse({'error': 'Book not found'}, status=404)
        except Exception as e:
            # Log the error here
            return JsonResponse({'error': 'An unexpected error occurred'}, status=500)

While this offers better organization through class structure, it still suffers from the same issue as function-based views: error-handling logic is duplicated across different view classes.

For class-based views, a more elegant approach is to override the dispatch method to handle exceptions centrally for all HTTP methods:

 
class BaseApiView(View):
    def dispatch(self, request, *args, **kwargs):
        try:
            return super().dispatch(request, *args, **kwargs)
        except Book.DoesNotExist:
            return JsonResponse({'error': 'Resource not found'}, status=404)
        except ValidationError as e:
            return JsonResponse({'error': 'Validation failed', 'details': e.message_dict}, status=400)
        except Exception as e:
            # Log the exception
            return JsonResponse({'error': 'Server error'}, status=500)

class BookDetailView(BaseApiView):
    def get(self, request, book_id):
        book = Book.objects.get(id=book_id)  # Will be caught by dispatch if fails
        return JsonResponse({...})

This pattern centralizes error handling at the class level and allows you to create a hierarchy of view classes with consistent error handling.

Django REST framework exception handling

If you're building a REST API with Django REST Framework (DRF), it provides a robust exception-handling system that converts exceptions into appropriate REST responses.

DRF automatically handles many Django and Python exceptions, mapping them to appropriate HTTP status codes. For example, Http404 becomes a 404 response, PermissionDenied becomes a 403 response, and so on.

You can customize exception handling by implementing exception handler functions:

api/exception_handlers.py
# api/exception_handlers.py
from rest_framework.views import exception_handler
from rest_framework.response import Response

def custom_exception_handler(exc, context):
    # First, use DRF's default handler
    response = exception_handler(exc, context)

    # If DRF didn't handle it, it's an unexpected error
    if response is None:
        return Response({
            'status': 'error',
            'message': 'An unexpected error occurred'
        }, status=500)

    # Customize the response format
    response.data = {
        'status': 'error',
        'message': str(exc),
        'details': response.data if hasattr(response, 'data') else None
    }

    return response

Then register this in your DRF settings:

 
# settings.py
REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'myapp.api.exception_handlers.custom_exception_handler',
}

This centralizes error handling across your entire API, ensuring consistent error responses regardless of where the exception occurs.

Creating custom exception middleware

One of the most effective patterns for handling errors in Django is to implement custom exception middleware. This centralizes error handling logic and ensures consistent responses across your entire application.

Instead of handling exceptions inside each view, middleware intercepts all requests and responses, allowing you to catch and handle exceptions globally:

middleware/exception_handler.py
import logging
from django.http import JsonResponse, HttpResponse
from django.template.response import TemplateResponse

logger = logging.getLogger(__name__)

class ExceptionMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)

    def process_exception(self, request, exception):
        # Log the exception
        logger.error(f"Unhandled exception: {str(exception)}", exc_info=True)

        # Check if this is an API request (based on URL or Accept header)
        is_api_request = request.path.startswith('/api/') or request.headers.get('Accept') == 'application/json'

        # For API requests, return JSON responses
        if is_api_request:
            if isinstance(exception, ValueError):
                return JsonResponse({
                    'status': 'error',
                    'message': str(exception)
                }, status=400)

            # Default to 500 for unexpected errors
            return JsonResponse({
                'status': 'error',
                'message': 'An internal server error occurred'
            }, status=500)

        # For regular requests, render appropriate error templates
        if isinstance(exception, Http404):
            return TemplateResponse(request, '404.html', status=404)

        # Default to 500 page
        return TemplateResponse(request, '500.html', status=500)

Register the middleware in your Django settings:

 
# settings.py
MIDDLEWARE = [
    # ... other middleware
    'myapp.middleware.exception_handler.ExceptionMiddleware',
]

This approach ensures all unhandled exceptions are caught and converted into appropriate responses, rather than resulting in the default Django error page or server crash.

Middleware-based error handling offers several key benefits: - Centralizes error handling logic in one place - Ensures consistent error responses - Reduces boilerplate in views - Provides a clean separation between business logic and error handling - Simplifies logging and monitoring

The middleware approach is particularly powerful when combined with custom exception classes, as it allows different types of errors to be handled differently based on their class.

Custom exception classes

Creating custom exception classes brings structure and context to error handling. Extending Django's built-in exceptions or Python's base Exception class allows you to create application-specific exceptions with meaningful status codes and messages.

exceptions.py
from django.core.exceptions import ValidationError

class ApplicationError(Exception):
    """Base class for application exceptions"""
    status_code = 500
    default_message = "An error occurred"

    def __init__(self, message=None, status_code=None):
        self.message = message or self.default_message
        if status_code is not None:
            self.status_code = status_code
        super().__init__(self.message)

class ResourceNotFoundError(ApplicationError):
    """Raised when a requested resource does not exist"""
    status_code = 404
    default_message = "Resource not found"

class ValidationFailedError(ApplicationError):
    """Raised when input validation fails"""
    status_code = 400
    default_message = "Invalid input data"

    def __init__(self, message=None, errors=None, status_code=None):
        super().__init__(message, status_code)
        self.errors = errors or {}

Then update your middleware to handle these custom exceptions:

middleware/exception_handler.py
from ..exceptions import ApplicationError

class ExceptionMiddleware:
    # ... existing code

    def process_exception(self, request, exception):
        # Handle custom application exceptions
        if isinstance(exception, ApplicationError):
            return JsonResponse({
                'status': 'error',
                'message': exception.message,
                **({"errors": exception.errors} if hasattr(exception, "errors") else {})
            }, status=exception.status_code)

        # ... handle other exceptions

Now you can raise these exceptions in your views for cleaner error handling:

 
from .exceptions import ResourceNotFoundError, ValidationFailedError

def book_detail(request, book_id):
    try:
        book = Book.objects.get(id=book_id)
    except Book.DoesNotExist:
        raise ResourceNotFoundError(f"Book with id {book_id} not found")

    # ... rest of view code

This approach creates a clean separation between error detection (in views) and error handling (in middleware). It also makes your code more expressive, as exception types clearly communicate the nature of errors.

Additional specialized error classes can be created for different scenarios:

 
class AuthenticationError(ApplicationError):
    """Raised when authentication fails"""
    status_code = 401
    default_message = "Authentication failed"

class AuthorizationError(ApplicationError):
    """Raised when a user lacks permissions"""
    status_code = 403
    default_message = "You don't have permission to perform this action"

class RateLimitExceededError(ApplicationError):
    """Raised when rate limits are exceeded"""
    status_code = 429
    default_message = "Rate limit exceeded"

To maintain consistency and clarity in your error handling, follow these best practices:

  • Create a base exception class that all application-specific exceptions inherit from
  • Include helpful default messages and appropriate status codes in each exception class
  • Allow for customization of messages and status codes when exceptions are raised
  • Add additional context when needed (e.g., validation details for validation errors)
  • Document each exception class's purpose and when it should be used

Custom exception classes make your code more readable and maintainable while ensuring consistent error responses throughout your application.

Handling form validation errors

Form validation is a common source of errors in Django applications. When users submit invalid data, your application needs to respond with clear, helpful error messages while preserving the user's input. Django's form system provides robust validation capabilities, but handling these errors effectively requires thoughtful implementation.

Server-side form validation

Django forms automatically validate data when you call form.is_valid(). For traditional HTML forms, the standard pattern involves checking validation status and responding accordingly:

 
from django.shortcuts import render, redirect
from .forms import BookForm

def create_book(request):
    if request.method == 'POST':
        form = BookForm(request.POST)
        if form.is_valid():
            book = form.save()
            return redirect('book_detail', book_id=book.id)
    else:
        form = BookForm()

    return render(request, 'books/create.html', {'form': form})

When validation fails, Django automatically populates form.errors with detailed error messages. The template can then display these errors to guide the user:

 
<form method="post">
    {% csrf_token %}

    {% if form.non_field_errors %}
        <div class="alert alert-danger">
            {% for error in form.non_field_errors %}
                {{ error }}
            {% endfor %}
        </div>
    {% endif %}

    {% for field in form %}
        <div class="form-group">
            {{ field.label_tag }}
            {{ field }}
            {% if field.errors %}
                <div class="text-danger">
                    {% for error in field.errors %}
                        {{ error }}
                    {% endfor %}
                </div>
            {% endif %}
        </div>
    {% endfor %}

    <button type="submit">Save</button>
</form>

This approach handles validation errors by re-rendering the form with error messages, making it clear to users what needs to be corrected while preserving their input.

API form validation

For API endpoints, you need to transform form validation errors into structured JSON responses. Here's an effective pattern:

 
from django.http import JsonResponse
from .forms import BookForm

def create_book_api(request):
    if request.method == 'POST':
        form = BookForm(request.POST)
        if form.is_valid():
            book = form.save()
            return JsonResponse({
                'status': 'success',
                'book_id': book.id
            })
        else:
            # Convert form errors to a structured response
            return JsonResponse({
                'status': 'error',
                'message': 'Validation failed',
                'errors': form.errors
            }, status=400)

    return JsonResponse({'error': 'Method not allowed'}, status=405)

However, Django's default form error serialization isn't ideal for APIs. The errors dictionary contains lists of errors for each field, and the format may include HTML. To create a cleaner API response, process the errors:

 
def create_book_api(request):
    if request.method == 'POST':
        form = BookForm(request.POST)
        if form.is_valid():
            book = form.save()
            return JsonResponse({
                'status': 'success',
                'book_id': book.id
            })
        else:
            # Process errors into a more API-friendly format
            field_errors = {}
            for field, errors in form.errors.items():
                field_name = field if field != '__all__' else 'nonFieldErrors'
                field_errors[field_name] = [str(e) for e in errors]

            return JsonResponse({
                'status': 'error',
                'message': 'Validation failed',
                'errors': field_errors
            }, status=400)

    return JsonResponse({'error': 'Method not allowed'}, status=405)

This produces a cleaner JSON response:

 
{
  "status": "error",
  "message": "Validation failed",
  "errors": {
    "title": ["This field is required."],
    "publicationYear": ["Enter a valid year."],
    "nonFieldErrors": ["Publication date cannot be in the future."]
  }
}

Integrating with custom exception classes

For better integration with your application's error handling system, you can raise custom exceptions for form validation errors:

 
from .exceptions import ValidationFailedError, MethodNotAllowedError

def create_book_api(request):
    if request.method == 'POST':
        form = BookForm(request.POST)
        if form.is_valid():
            book = form.save()
            return JsonResponse({
                'status': 'success',
                'book_id': book.id
            })
        else:
            # Convert form errors and raise custom exception
            field_errors = {}
            for field, errors in form.errors.items():
                field_name = field if field != '__all__' else 'nonFieldErrors'
                field_errors[field_name] = [str(e) for e in errors]

            raise ValidationFailedError(
                message="Book validation failed",
                errors=field_errors
            )

    raise MethodNotAllowedError("Method not allowed")

This approach leverages your middleware-based error handling, ensuring form validation errors are handled consistently with other application errors. The middleware can then transform these exceptions into appropriate responses without additional code in each view.

Django REST framework serializer validation

If you're using Django REST Framework, its serializers offer validation similar to Django forms but with better API integration:

 
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .serializers import BookSerializer

@api_view(['POST'])
def create_book_api(request):
    serializer = BookSerializer(data=request.data)
    if serializer.is_valid():
        book = serializer.save()
        return Response({
            'status': 'success',
            'book_id': book.id
        })

    return Response({
        'status': 'error',
        'message': 'Validation failed',
        'errors': serializer.errors
    }, status=status.HTTP_400_BAD_REQUEST)

DRF's serializer validation system automatically converts errors to a JSON-friendly format, eliminating the need for manual error processing in most cases.

Advanced ModelForm validation

For complex forms, you may need to handle validations at multiple levels:

 
class BookForm(forms.ModelForm):
    class Meta:
        model = Book
        fields = ['title', 'author', 'publication_date', 'genre']

    def clean_title(self):
        """Field-level validation"""
        title = self.cleaned_data.get('title')
        if title and len(title) < 3:
            raise ValidationError("Title must be at least 3 characters long")
        return title

    def clean(self):
        """Form-level validation for field interdependencies"""
        cleaned_data = super().clean()
        publication_date = cleaned_data.get('publication_date')
        genre = cleaned_data.get('genre')

        if publication_date and genre:
            # Genre-specific publication date validation
            if genre.name == "Science Fiction" and publication_date.year < 1900:
                self.add_error('publication_date', 
                               "Science fiction books cannot be published before 1900")

        return cleaned_data

ModelForm validation occurs in this sequence:

  1. Field-level clean methods (clean_fieldname)
  2. Form-level clean method (clean)
  3. Model-level validation during save()

Each level can add errors that will appear in form.errors after calling is_valid().

Custom field validators

For reusable validation logic, Django supports field validators that can be applied to model fields and form fields:

 
def validate_future_date(value):
    if value > date.today():
        raise ValidationError("Date cannot be in the future")

class Book(models.Model):
    title = models.CharField(max_length=200)
    publication_date = models.DateField(validators=[validate_future_date])

These validators are automatically applied during form validation, and errors are collected and displayed just like other validation errors.

Final thoughts

Effective error handling stands at the core of quality Django applications. These patterns create an error management approach that gives users clear messages, prevents crashes, maintains consistency, provides detailed logs, and protects your system. Start with solid error handling early and enjoy a more resilient, maintainable application. Great error handling goes beyond catching exceptions—it ensures users have a positive experience even when problems occur.

Author's avatar
Article by
Stanley Ulili
Stanley Ulili is a technical educator at Better Stack based in Malawi. He specializes in backend development and has freelanced for platforms like DigitalOcean, LogRocket, and AppSignal. Stanley is passionate about making complex topics accessible to developers.
Got an article suggestion? Let us know
Next article
A Comprehensive Guide to Logging in Python
Python provides a built-in logging module in its standard library that provides comprehensive logging capabilities for Python programs
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github