How It Works Features Pricing Blog Error Guides
Log In Start Free Trial
Django · Python

Fix DoesNotExist: Product matching query does not exist. in Django

This error means .get() was called on a queryset but no row matched the filter criteria. Fix it by using get_object_or_404() for views that should return a 404 page, wrapping .get() in a try/except block, or using .filter().first() which returns None instead of raising an exception when no match is found.

Reading the Stack Trace

Traceback (most recent call last): File "/venv/lib/python3.11/site-packages/django/core/handlers/exception.py", line 55, in inner response = get_response(request) File "/venv/lib/python3.11/site-packages/django/core/handlers/base.py", line 197, in _get_response response = wrapped_callback(request, *callback_args, **callback_kwargs) File "/app/products/views.py", line 11, in product_detail product = Product.objects.get(slug=slug) File "/venv/lib/python3.11/site-packages/django/db/models/manager.py", line 87, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) File "/venv/lib/python3.11/site-packages/django/db/models/query.py", line 637, in get raise self.model.DoesNotExist( products.models.Product.DoesNotExist: Product matching query does not exist.

Here's what each line means:

Common Causes

1. Using .get() without error handling

The view uses .get() which raises DoesNotExist if no matching record is found, instead of returning a 404 or handling the case gracefully.

def product_detail(request, slug):
    product = Product.objects.get(slug=slug)  # Crashes if slug not found
    return render(request, 'products/detail.html', {'product': product})

2. Deleted or unpublished record

The record existed previously but was deleted or marked as unpublished, and the URL or link still references it.

def product_detail(request, slug):
    # Does not filter by is_published, so published URLs may break
    product = Product.objects.get(slug=slug, is_published=True)

3. Case-sensitive slug mismatch

The slug in the URL uses different casing than the database value, and the default lookup is case-sensitive.

# URL: /products/My-Product/
# Database slug: my-product
product = Product.objects.get(slug='My-Product')  # Case mismatch

The Fix

Use get_object_or_404() which catches DoesNotExist and returns a proper 404 response. This is Django's idiomatic pattern for detail views. For non-view code, use try/except or .filter().first() depending on whether the absence is expected.

Before (broken)
def product_detail(request, slug):
    product = Product.objects.get(slug=slug)
    return render(request, 'products/detail.html', {'product': product})
After (fixed)
from django.shortcuts import get_object_or_404

def product_detail(request, slug):
    product = get_object_or_404(Product, slug=slug)
    return render(request, 'products/detail.html', {'product': product})

# Alternative: try/except for custom handling
# from django.http import Http404
# def product_detail(request, slug):
#     try:
#         product = Product.objects.get(slug=slug)
#     except Product.DoesNotExist:
#         raise Http404('Product not found')
#     return render(request, 'products/detail.html', {'product': product})

Testing the Fix

import pytest
from django.test import TestCase, Client
from products.models import Product


class TestProductDetail(TestCase):
    def setUp(self):
        self.client = Client()
        self.product = Product.objects.create(
            name='Test Widget',
            slug='test-widget',
            price=9.99,
        )

    def test_existing_product_returns_200(self):
        response = self.client.get('/products/test-widget/')
        assert response.status_code == 200
        self.assertContains(response, 'Test Widget')

    def test_nonexistent_product_returns_404(self):
        response = self.client.get('/products/does-not-exist/')
        assert response.status_code == 404

    def test_deleted_product_returns_404(self):
        self.product.delete()
        response = self.client.get('/products/test-widget/')
        assert response.status_code == 404

Run your tests:

pytest

Pushing Through CI/CD

git checkout -b fix/doesnotexist-product-detail,git add products/views.py,git commit -m "fix: use get_object_or_404 to return 404 for missing products",git push origin fix/doesnotexist-product-detail

Your CI config should look something like this:

name: CI
on:
  pull_request:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_DB: test_db
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
        ports:
          - 5432:5432
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: 'pip'
      - run: pip install -r requirements.txt
      - run: pytest --tb=short -q

The Full Manual Process: 18 Steps

Here's every step you just went through to fix this one bug:

  1. Notice the error alert or see it in your monitoring tool
  2. Open the error dashboard and read the stack trace
  3. Identify the file and line number from the stack trace
  4. Open your IDE and navigate to the file
  5. Read the surrounding code to understand context
  6. Reproduce the error locally
  7. Identify the root cause
  8. Write the fix
  9. Run the test suite locally
  10. Fix any failing tests
  11. Write new tests covering the edge case
  12. Run the full test suite again
  13. Create a new git branch
  14. Commit and push your changes
  15. Open a pull request
  16. Wait for code review
  17. Merge and deploy to production
  18. Monitor production to confirm the error is resolved

Total time: 30-60 minutes. For one bug.

Or Let bugstack Fix It in Under 2 minutes

Every step above? bugstack does it automatically.

Step 1: Install the SDK

pip install bugstack

Step 2: Initialize

import bugstack

bugstack.init(api_key=os.environ["BUGSTACK_API_KEY"])

Step 3: There is no step 3.

bugstack handles everything from here:

  1. Captures the stack trace and request context
  2. Pulls the relevant source files from your GitHub repo
  3. Analyzes the error and understands the code context
  4. Generates a minimal, verified fix
  5. Runs your existing test suite
  6. Pushes through your CI/CD pipeline
  7. Deploys to production (or opens a PR for review)

Time from error to fix deployed: Under 2 minutes.

Human involvement: zero.

Try bugstack Free →

No credit card. 5-minute setup. Cancel anytime.

Deploying the Fix (Manual Path)

  1. Run the full test suite locally to confirm 404 handling works.
  2. Open a pull request with the view fix.
  3. Wait for CI checks to pass on the PR.
  4. Have a teammate review and approve the PR.
  5. Merge to main and verify 404 pages render correctly in staging.

Frequently Asked Questions

BugStack runs the fix through your existing test suite, generates tests for both existing and missing records, and validates that 404 pages render correctly before marking it safe to deploy.

BugStack never pushes directly to production. Every fix goes through a pull request with full CI checks, so your team can review it before merging.

Use .filter().first() when the absence of a record is a normal case (returns None), and get_object_or_404 when the record should exist and its absence means a bad URL.

If .get() can match multiple rows, either add a unique constraint to the field, or use .filter().first() to return just one. get_object_or_404 also raises MultipleObjectsReturned if the lookup is not unique.