Error Handling¶
Hypern provides comprehensive error handling with HTTPException subclasses,
per-type ExceptionHandler registration, and an @error_boundary decorator
for local exception capture.
Built-in Exceptions¶
Every HTTP exception inherits from HTTPException and carries a status_code,
detail message, optional data payload, and optional headers. The
exception envelope returned to clients is:
Available exceptions (all importable from hypern):
| Exception | Status | Purpose |
|---|---|---|
HTTPException |
any | Base class — pass any status code |
BadRequest |
400 | General request errors |
Unauthorized |
401 | Authentication required |
Forbidden |
403 | Access denied |
NotFound |
404 | Resource not found |
MethodNotAllowed |
405 | HTTP method not allowed |
Conflict |
409 | Resource conflict |
UnprocessableEntity |
422 | Validation failures |
TooManyRequests |
429 | Rate limit exceeded |
InternalServerError |
500 | Server errors |
ServiceUnavailable |
503 | Service unavailable |
Helpers: ExceptionHandler (per-type handler registry), error_boundary
(handler-local exception capture decorator).
Basic Error Handling¶
from hypern import Hypern, NotFound, BadRequest
app = Hypern()
@app.get("/users/:id")
def get_user(req, res, ctx):
user_id = req.param("id")
user = find_user_by_id(user_id)
if not user:
raise NotFound(f"User {user_id} not found")
res.json(user)
@app.post("/users")
def create_user(req, res, ctx):
body = req.json()
if not body.get("email"):
raise BadRequest("Email is required", data={"field": "email"})
res.status(201).json({"message": "User created"})
Custom Error Classes¶
Subclass HTTPException to add domain-specific errors. The framework will
serialize any subclass through the same envelope:
from hypern import HTTPException
class DatabaseError(HTTPException):
def __init__(self, message: str):
super().__init__(500, message, data={"code": "DB_ERROR"})
class PaymentError(HTTPException):
def __init__(self, message: str, amount: float | None = None):
super().__init__(
402,
message,
data={"code": "PAYMENT_ERROR", "amount": amount},
)
@app.post("/charge")
def charge_user(req, res, ctx):
body = req.json()
amount = body.get("amount")
try:
process_payment(amount)
except Exception as e:
raise PaymentError(str(e), amount)
Error Response Format¶
Raising an HTTPException (or any subclass) returns JSON automatically:
raise NotFound("Resource not found")
# Response body
{
"error": true,
"status_code": 404,
"message": "Resource not found"
}
ExceptionHandler — Per-Type Handlers¶
ExceptionHandler lets you register custom handlers for specific exception
classes and wire them into the application:
from hypern import Hypern, ExceptionHandler, HTTPException, BadRequest
app = Hypern()
handler = ExceptionHandler()
@handler.handle(HTTPException)
def http_handler(req, res, exc):
res.status(exc.status_code).json(exc.to_dict())
@handler.handle(ValueError)
def value_error_handler(req, res, exc):
res.status(400).json({"error": True, "message": str(exc)})
app.set_exception_handler(handler)
ExceptionHandler.handle(exc_class) returns a decorator. add_handler(...)
registers a handler programmatically. set_default_handler(...) catches
anything that has no specific handler.
@error_boundary Decorator¶
Wrap a single handler to catch exceptions locally instead of relying on global
plumbing. Handlers wrapped in @error_boundary raise HTTPException straight
to JSON, and route everything else through the registered default handler:
from hypern import Hypern, error_boundary, NotFound
app = Hypern()
@app.get("/users/:id")
@error_boundary
def get_user(req, res, ctx):
user = db.fetch_user(req.param("id"))
if user is None:
raise NotFound("User not found")
res.json(user)
Validation Error Details¶
UnprocessableEntity (or BadRequest) is the right tool for input validation:
import msgspec
from hypern import Json, UnprocessableEntity, Conflict
class UserSchema(msgspec.Struct):
name: str
email: str
age: int
@app.post("/users")
def create_user(req, res, ctx, body: UserSchema = Json()):
if body.age < 18:
raise UnprocessableEntity(
"User must be at least 18 years old",
data={"field": "age", "value": body.age},
)
if user_exists(body.email):
raise Conflict("Email already registered",
data={"field": "email"})
res.status(201).json({"message": "User created"})
Persistence Error Handling¶
from hypern import Hypern, Inject, Conflict, InternalServerError
app = Hypern()
class ProductRepository:
def create(self, product):
# The application's persistence adapter owns this operation.
return {"id": "new-product", **product}
app.provide(ProductRepository, ProductRepository())
@app.post("/products")
def create_product(
req,
res,
ctx,
product_repository: ProductRepository = Inject(),
):
body = req.json()
try:
product = product_repository.create(body)
res.status(201).json(product)
except Exception as e:
if "unique constraint" in str(e):
raise Conflict("Product name already exists")
raise InternalServerError(f"Persistence error: {str(e)}")
Async Error Handling¶
from hypern import Hypern, HTTPException
import asyncio
app = Hypern()
@app.get("/async-data")
async def get_async_data(req, res, ctx):
try:
data = await fetch_remote_data()
res.json(data)
except asyncio.TimeoutError:
raise HTTPException(504, "Request timeout")
except Exception as e:
raise HTTPException(502, str(e))
Try-Catch Patterns¶
Option 1: Let exceptions propagate¶
from hypern import NotFound
@app.get("/users/:id")
def get_user(req, res, ctx):
user = db.fetch_user(req.param("id"))
if user is None:
raise NotFound("User not found")
res.json(user)
Option 2: Handle and transform¶
from hypern import NotFound, InternalServerError
@app.get("/users/:id")
def get_user(req, res, ctx):
try:
user = db.fetch_user(req.param("id"))
if user is None:
raise NotFound("User not found")
res.json(user)
except NotFound:
raise # Re-raise
except Exception as e:
raise InternalServerError(str(e))
Option 3: Return error response¶
@app.get("/users/:id")
def get_user(req, res, ctx):
try:
user = db.fetch_user(req.param("id"))
if user is None:
res.status(404).json({"error": "User not found"})
return
res.json(user)
except Exception as e:
res.status(500).json({"error": str(e)})
Error Logging¶
import logging
from hypern import Hypern, HTTPException
logger = logging.getLogger(__name__)
app = Hypern()
@app.middleware("after_route")
def log_errors(req, res, ctx, next):
try:
next()
except HTTPException as e:
logger.warning(f"{req.method} {req.path} - {type(e).__name__}: {e.detail}")
raise
except Exception as e:
logger.error(f"Unexpected error on {req.method} {req.path}: {e}", exc_info=True)
raise
Best Practices¶
- Use appropriate status codes — 400, 401, 403, 404, 409, 422, 500, etc.
- Provide machine-readable codes — put them in the
datafield, not in the message. - Include request IDs — trace errors through logs using
ctx.request_id. - Don't expose internals — hide stack traces in production.
- Log all errors — comprehensive logging for debugging.
- Handle async errors — properly catch exceptions in async handlers.
- Validate early — catch errors at the validation stage.
- Centralize — register one
ExceptionHandlerinstead of try/except in every route. - Test error scenarios — include error handling in tests.
- Document errors — list the error envelope in your API docs.