Guide for creating exceptions using fastapi-problem that are automatically converted to RFC 9457 Problem Details responses.
Scanned 2/12/2026
Install via CLI
openskills install majiayu000/claude-skill-registry---
name: exceptions
description: Guide for creating exceptions using fastapi-problem that are automatically converted to RFC 9457 Problem Details responses.
---
# Exception Creation
Use this skill when creating exceptions that are automatically converted to RFC 9457 Problem Details responses.
For comprehensive coding guidelines, see `AGENTS.md` in the repository root.
## Base Exception Classes
The project uses `fastapi-problem` base classes from `app/exceptions/base.py`:
```python
from fastapi_problem.error import (
BadRequestProblem,
ConflictProblem,
ForbiddenProblem,
NotFoundProblem,
ServerProblem,
UnauthorisedProblem,
UnprocessableProblem,
)
```
| Base Class | Status Code | Use Case |
|------------|-------------|----------|
| `NotFoundProblem` | 404 | Resource not found |
| `ConflictProblem` | 409 | Duplicate resource, state conflict |
| `BadRequestProblem` | 400 | Invalid request (cursor, parameter) |
| `ForbiddenProblem` | 403 | Access denied |
| `UnauthorisedProblem` | 401 | Authentication required |
| `UnprocessableProblem` | 422 | Validation failure |
| `ServerProblem` | 500 | Internal server error |
## Creating Resource-Specific Exceptions
Create new exceptions in `app/exceptions/`:
```python
# app/exceptions/resource.py
"""
Resource-related exceptions.
"""
from fastapi_problem.error import ConflictProblem, NotFoundProblem
class ResourceNotFoundError(NotFoundProblem):
"""
Raised when a resource cannot be found.
"""
title = "Resource not found"
class ResourceAlreadyExistsError(ConflictProblem):
"""
Raised when attempting to create a duplicate resource.
"""
title = "Resource already exists"
```
## Exporting Exceptions
Export new exceptions from `app/exceptions/__init__.py`:
```python
from app.exceptions.base import (
BadRequestProblem,
ConflictProblem,
ForbiddenProblem,
NotFoundProblem,
ServerProblem,
UnauthorisedProblem,
UnprocessableProblem,
)
from app.exceptions.resource import ResourceAlreadyExistsError, ResourceNotFoundError
__all__ = [
"BadRequestProblem",
"ConflictProblem",
"ForbiddenProblem",
"NotFoundProblem",
"ResourceAlreadyExistsError",
"ResourceNotFoundError",
"ServerProblem",
"UnauthorisedProblem",
"UnprocessableProblem",
]
```
## Using Exceptions
Import from the package root:
```python
# In routers and services
from app.exceptions import ResourceNotFoundError, ResourceAlreadyExistsError
```
Raise exceptions in services:
```python
async def get_resource(self, user_id: str) -> Resource:
snapshot = await doc_ref.get()
if not snapshot.exists:
raise ResourceNotFoundError("Resource not found")
return Resource(**snapshot.to_dict())
```
## Exception Handling in Routers
Re-raise exceptions to let handlers convert them:
```python
@router.get("/")
async def get_resource(
current_user: CurrentUser,
service: ResourceServiceDep,
) -> Resource:
try:
return await service.get_resource(current_user.uid)
except (HTTPException, ResourceNotFoundError):
raise
except Exception:
logger.exception("Error getting resource", extra={"user_id": current_user.uid})
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve resource"
) from None
```
## RFC 9457 Problem Details Response
Exceptions are automatically converted to Problem Details format:
```json
{
"type": "about:blank",
"title": "Resource not found",
"status": 404,
"detail": "Resource not found"
}
```
The exception handler in `app/core/exception_handler.py`:
- Uses `fastapi-problem` singleton `eh` with pre/post hooks
- Adds `X-Request-ID` to all error responses
- Adds `$schema` field and `Link` header with `rel="describedBy"` to error responses
- Supports CBOR error responses via `CBORProblemPostHook`
- Strips extras from 5xx errors in production via `StripExtrasPostHook`
## Custom Detail Message
Pass a custom message when raising:
```python
raise ResourceNotFoundError(detail="Resource with ID 'abc123' was not found")
```
## Naming Convention
Use descriptive names with `Error` suffix:
- `{Resource}NotFoundError`
- `{Resource}AlreadyExistsError`
- `{Resource}InvalidError`
- `{Resource}ExpiredError`
## Testing
Test exception behavior:
```python
def test_returns_404_when_not_found(
client: TestClient,
with_fake_user: None,
mock_resource_service: AsyncMock,
) -> None:
mock_resource_service.get_resource.side_effect = ResourceNotFoundError()
response = client.get("/v1/resource")
assert response.status_code == 404
body = response.json()
assert body["title"] == "Resource not found"
assert body["status"] == 404
```
No comments yet. Be the first to comment!