affaan-m/ECC · warning · HTTPException
Email already registered
Error message
Email already registered
What it means
In the `create_user` route handler, the service layer raises a domain exception `DuplicateUserError`; the handler maps it to `HTTPException(400, "Email already registered")`. The 400 (not 409) is the documented contract for this codebase — `DuplicateUserError` is the canonical "email taken" signal from `UserService.create`.
Source
Thrown at skills/fastapi-patterns/SKILL.md:248
# app/routers/users.py
from typing import Annotated
from fastapi import APIRouter, HTTPException, Query, status
from fastapi.security import OAuth2PasswordRequestForm
from app.dependencies import ActiveUserDep, DbDep
from app.schemas.user import UserCreate, UserResponse, UserUpdate, UserListResponse
from app.services.user_service import DuplicateUserError, UserService
router = APIRouter()
@router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(payload: UserCreate, db: DbDep) -> UserResponse:
service = UserService(db)
try:
return await service.create(payload)
except DuplicateUserError:
raise HTTPException(status_code=400, detail="Email already registered")
@router.get("/me", response_model=UserResponse)
async def get_me(current_user: ActiveUserDep) -> UserResponse:
return current_user
@router.get("/", response_model=UserListResponse)
async def list_users(
db: DbDep,
current_user: ActiveUserDep,
skip: Annotated[int, Query(ge=0)] = 0,
limit: Annotated[int, Query(ge=1, le=100)] = 20,
) -> UserListResponse:
service = UserService(db)
users, total = await service.list(skip=skip, limit=limit)
return UserListResponse(total=total, items=users)
View on GitHub (pinned to 01e15490f0)
Solutions
- Front-end: on 400 with this detail, show "an account with this email already exists" and offer login/password-reset.
- Confirm there is a unique constraint on `users.email` at the DB level — without it `DuplicateUserError` can never fire and you get a silent second row.
- Make sure `UserService.create` actually raises `DuplicateUserError` on `IntegrityError` (and not a generic 500).
- For races, consider 409 Conflict if your API standard prefers it — but keep the service exception the source of truth.
Example fix
# before
except DuplicateUserError:
raise HTTPException(status_code=400, detail="Email already registered")
# after — 409 is more conventional for conflict; keep detail stable for clients
except DuplicateUserError:
raise HTTPException(status_code=409, detail="Email already registered") Defensive patterns
Strategy: try-catch
Validate before calling
null
Type guard
null
Try / catch
from fastapi import HTTPException
try:
resp = await client.post('/users/', json=payload)
except HTTPException as e:
if e.status_code == 400 and 'already registered' in e.detail:
suggest_login(payload['email'])
raise Prevention
- Guarantee a DB unique constraint on `users.email` so the service can detect duplicates reliably.
- Map `DuplicateUserError` to HTTP in one place (exception handler) instead of per route.
- Free-text email availability check (debounced) before submit to reduce 400s.
When it happens
Trigger: POST `/users/` (or `/`) with an `email` that already exists in the users table — `UserService.create` inserts, hits the unique constraint, and raises `DuplicateUserError`. Race condition: two concurrent registrations for the same email.
Common situations: User registers twice (typo, forgot they had an account). Test suite does not clean up users between runs. Seed data inserts an email a test then tries to create.
Related errors
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/2b2911eb4d84f44e.
Report an issue: GitHub.