Error Handling¶
Introduction¶
When you start a new Arvel project, error handling is already configured for you. Arvel turns exceptions raised during request handling into consistent JSON error responses. You raise a semantic exception — "not found", "forbidden", "validation failed" — and the framework's handler serializes it with the right status code and a uniform body. You never assemble error responses by hand.
The Error Envelope¶
Every HTTP exception serializes to the same envelope:
{
"error": {
"code": "NOT_FOUND",
"message": "Post not found.",
"details": [
{"field": "email", "issue": "The email has already been taken."}
]
}
}
codeis a stable, machine-readable identifier (UPPER_SNAKE_CASE).messageis a human-readable, single-sentence description.detailsis optional — present only when the exception carries field-level issues, as validation errors do.
This shape is identical whether the error came from your code, a form request, or FastAPI's own body parsing, so clients only ever parse one format.
HTTP Exceptions¶
Available Exceptions¶
Arvel provides a typed exception for each common HTTP status. Each carries a fixed status code and machine code:
| Exception | Status | code |
|---|---|---|
BadRequestException | 400 | BAD_REQUEST |
UnauthenticatedException | 401 | UNAUTHENTICATED |
AuthorizationException | 403 | FORBIDDEN |
NotFoundException | 404 | NOT_FOUND |
MethodNotAllowedException | 405 | METHOD_NOT_ALLOWED |
ConflictException | 409 | CONFLICT |
ValidationException | 422 | VALIDATION_FAILED |
UnprocessableException | 422 | UNPROCESSABLE |
ThrottleException | 429 | TOO_MANY_REQUESTS |
ServerErrorException | 500 | INTERNAL_ERROR |
HttpException | 500 (overridable) | INTERNAL_ERROR |
Note
All of these are importable from the top-level arvel package except UnprocessableException, which lives in arvel.http.exceptions. The CsrfMismatchException raised by VerifyCsrf (419, CSRF_MISMATCH) lives in arvel.http.middleware.
Throwing Exceptions¶
Raise an exception anywhere during request handling, and the handler converts it to the matching response:
from arvel import NotFoundException, ValidationException
from arvel.http.exceptions import UnprocessableException
raise NotFoundException("Post not found.")
raise ValidationException(
"Validation failed.",
details=[{"field": "email", "issue": "must be unique"}],
)
ThrottleException requires a retry_after_seconds, which the handler emits as a Retry-After header:
Where Exceptions Come From¶
You'll often encounter these exceptions without raising them yourself:
find_or_fail(...)and missing route model bindings surface as404s.- Form request rule failures raise
ValidationException(422). - A failed
authorize()or a denied gate check raisesAuthorizationException(403). - The
Authenticatemiddleware with no user raisesUnauthenticatedException(401).
Custom HTTP Exceptions¶
For your own domain errors, subclass HttpException and set the status and code:
from arvel import HttpException
class PaymentRequiredException(HttpException):
status_code = 402
code = "PAYMENT_REQUIRED"
Then raise it like any other:
Warning
Never put internal detail — SQL, stack traces, file paths, secrets — into the message of a client-facing exception. Keep messages user-appropriate and log the internals.
The Exception Handler¶
The exception handler is registered automatically when the application boots (into_asgi()). It:
- Serializes
HttpExceptionsubclasses to the envelope with their status code. - Normalizes FastAPI's
RequestValidationError(body-parsing failures) into the same422shape. - Catches any other unhandled
Exceptionand returns a generic500with the body"Something went wrong"— no stack traces, SQL, or internal paths leak to the client, in any environment.
Reporting and Logging¶
The handler logs as it works: handled HttpExceptions are logged at warning level (with auth and cookie headers redacted), and unhandled exceptions are logged at error level with the exception attached, before the generic 500 is returned. See Logging.
Custom Translators¶
To map a third-party or library exception to an HTTP response, register a translator — a callable that converts the exception into an HttpException. Resolve HttpExceptionHandler from the container in a provider's boot() and add the translator:
async def boot(self) -> None:
handler = self.app.make(HttpExceptionHandler)
handler.add_translator(SomeLibraryError, lambda exc: NotFoundException(str(exc)))
Arvel registers a few translators by default — for instance, the ORM's ModelNotFoundError is translated to NotFoundException, and the auth package's exceptions are mapped to their HTTP equivalents.
Problem Details (RFC 7807)¶
Arvel also ships a ProblemDetailsHandler (in arvel.http.problem_details) that renders errors as application/problem+json per RFC 7807. It's opt-in: bind it in place of the default HttpExceptionHandler in the container.
Warning
ProblemDetailsHandler registers handlers for HttpException and validation errors but does not install a catch-all for unhandled Exceptions. If you opt in, make sure unexpected errors are handled elsewhere so they don't escape as raw 500s.