Errors & Exception Handling¶
Django Ninja lets you install custom exception handlers to control exactly what response is returned when an exception is raised (or handled) inside a view.
Default exception handlers¶
Out of the box, Django Ninja registers handlers for the following exceptions. Whenever one of these is raised anywhere in your view (or a dependency, like a resolver or auth callback), the matching handler builds the response automatically.
| Exception | Handler behavior |
|---|---|
ninja.errors.HttpError |
Returns {"detail": <message>} with the status code given to the exception. |
ninja.errors.ValidationError |
Returns {"detail": <errors>} with a 422 status. |
django.http.Http404 |
Returns {"detail": "Not Found"} with a 404 status (includes exception details when DEBUG=True). |
Exception (anything else) |
If settings.DEBUG is True, returns a plain-text traceback with a 500 status. Otherwise, re-raises the exception so Django's normal error handling takes over (error logging, email to ADMINS, etc). |
ninja.errors.AuthenticationError and ninja.errors.AuthorizationError are both subclasses of HttpError (401 and 403 respectively), so they're handled by the HttpError handler unless you register something more specific. AuthenticationError is raised automatically by the authentication layer when a request fails to authenticate or an auth callback returns False; AuthorizationError isn't raised by Ninja itself — it's there for you to raise from inside a view or dependency when a permission check fails. ninja.errors.Throttled is also an HttpError subclass (429), raised by the throttling layer.
Throwing HTTP errors¶
The simplest way to return an error response from inside a view is to raise HttpError:
from ninja import NinjaAPI
from ninja.errors import HttpError
api = NinjaAPI()
@api.get("/some/resource")
def some_operation(request):
if not request.user.is_staff:
raise HttpError(503, "Service Unavailable. Please retry later.")
return {"message": "Hello"}
HttpError takes a status_code and a message, and results in a {"detail": message} JSON response with that status.
Custom exception handlers¶
For anything more involved than a plain HttpError, register a handler for your own exception class with api.exception_handler. This is useful, for example, when a view depends on an external service that's expected to be unavailable sometimes — instead of returning a generic 500, you can catch it and return something friendlier.
import random
from ninja import NinjaAPI
api = NinjaAPI()
class ServiceUnavailableError(Exception):
pass
@api.exception_handler(ServiceUnavailableError)
def service_unavailable(request, exc):
return api.create_response(
request,
{"message": "Please retry later"},
status=503,
)
@api.get("/service")
def some_operation(request):
if random.choice([True, False]):
raise ServiceUnavailableError()
return {"message": "Hello"}
A handler function always takes two arguments:
request— the DjangoHttpRequestexc— the raised exception instance
and must return an HttpResponse. api.create_response() is a convenience for building one that goes through the API's configured renderer, but you can return any HttpResponse (or subclass) directly.
exception_handler is a thin wrapper over api.add_exception_handler(exc_class, handler), which you can call directly instead of using it as a decorator.
Handler lookup follows the MRO
When an exception is raised, Ninja walks the exception's class __mro__ and uses the first registered handler it finds. So registering a handler for a base class also covers its subclasses — for example, overriding HttpError covers AuthenticationError, AuthorizationError and Throttled too, unless you also register something more specific for one of them.
Overriding the default handlers¶
Because the built-ins above are just handlers registered for particular exception classes, you override any of them the same way — register your own handler for that class:
from django.http import HttpResponse
from ninja import NinjaAPI
from ninja.errors import AuthenticationError
api = NinjaAPI()
@api.exception_handler(AuthenticationError)
def on_auth_error(request, exc):
return HttpResponse("Please sign in", status=401)
Customizing validation errors¶
Request validation failures raise ninja.errors.ValidationError (not to be confused with pydantic.ValidationError) and are handled by default with a 422 response shaped like:
Overriding the response shape¶
Override the ValidationError handler the same way as any other:
from django.http import HttpResponse
from ninja import NinjaAPI
from ninja.errors import ValidationError
api = NinjaAPI()
@api.exception_handler(ValidationError)
def validation_errors(request, exc: ValidationError):
return HttpResponse("Invalid input", status=422)
Customizing the errors list itself¶
If you need more control over how validation errors are built — for example, referencing the schema tied to the field that failed — override validation_error_from_error_contexts on a NinjaAPI subclass instead. It receives a list of ValidationErrorContext objects (one per parameter source — path, query, body, etc. — that failed) and must return a ValidationError:
from typing import Any
from ninja import NinjaAPI
from ninja.errors import ValidationError, ValidationErrorContext
class CustomNinjaAPI(NinjaAPI):
def validation_error_from_error_contexts(
self, error_contexts: list[ValidationErrorContext],
) -> ValidationError:
custom_error_infos: list[dict[str, Any]] = []
for context in error_contexts:
model = context.model
param_source = model.__ninja_param_source__
for e in context.pydantic_validation_error.errors(
include_url=False, include_context=False, include_input=False
):
custom_error_infos.append(
{"loc": (param_source, *e["loc"]), "msg": e["msg"], "type": e["type"]}
)
return ValidationError(custom_error_infos)
api = CustomNinjaAPI()
Each ValidationErrorContext exposes:
pydantic_validation_error— the underlyingpydantic.ValidationErrormodel— theParamModelthat failed validation, whose__ninja_param_source__tells you which part of the request it came from ("path","query","body", etc.)
The ValidationError returned from validation_error_from_error_contexts is then passed through the normal ValidationError handler (default, or your own override), so both hooks can be combined.
Unhandled exceptions¶
Anything that isn't caught by a registered handler falls through to the Exception handler:
- with
DEBUG=True, the response is a500with a plain-text traceback — handy when debugging from the console or from Swagger UI - with
DEBUG=False, the exception is re-raised and handled by Django's normal machinery (500 page, error logging, admin emails, etc.)
You can override this too, for example to always return JSON instead of a traceback:
import logging
from ninja import NinjaAPI
api = NinjaAPI()
logger = logging.getLogger("django")
@api.exception_handler(Exception)
def catch_all(request, exc: Exception):
logger.exception(exc)
return api.create_response(
request,
{"detail": "Internal server error"},
status=500,
)
Be careful overriding Exception globally — it will swallow errors you didn't anticipate too, so make sure logging still happens, as above.
Handlers are per-NinjaAPI instance
Exception handlers are registered on the NinjaAPI instance, not globally — Router has no exception_handler of its own. If you run multiple NinjaAPI instances, each needs its own exception_handler registrations.