Короткий ответ
Ошибку нужно перехватывать явным except на конкретный тип исключения (например, IntegrityError в Django/SQLAlchemy), откатывать текущую транзакцию и возвращать понятный ответ или бизнес-исключение, а не «проглатывать» любое исключение широким except.
Как это работает подробнее
При нарушении constraint СУБД отклоняет операцию и возвращает ошибку с конкретным кодом (например, SQLSTATE 23505 для unique_violation в PostgreSQL), которую драйвер оборачивает в своё исключение; транзакция переходит в состояние «aborted» до ROLLBACK.
Пример кода
example.py
from django.db import IntegrityError, transaction
def create_user(email: str, name: str) -> "User | None":
try:
with transaction.atomic():
return User.objects.create(email=email, name=name)
except IntegrityError:
# unique-ограничение на email нарушено, транзакция уже откатана Django
return None # вызывающий код превращает это в HTTP 409Что использовать на практике
- Практика. В веб-приложении типичный паттерн — перехватить конкретное исключение и преобразовать его в понятный ответ пользователю (например, HTTP 409 Conflict «email уже занят»), вместо падения с 500 ошибкой.
Подводные камни
- Ошибка. Ловят голый except Exception и просто логируют, теряя возможность вернуть осмысленный ответ; не делают rollback после перехваченной ошибки, получая новую ошибку «current transaction is aborted» на следующем запросе.