Errors
Application errors are stable public outcomes. Define them once, raise them from use cases, and declare which contracts may expose them.
Define and declare an error
from tenchi.errors import AppError, ErrorDef
todo_not_found = ErrorDef(
code="TODO_NOT_FOUND",
status=404,
message="Todo not found",
)
get_todo_contract = contract(
method="GET",
path="/todos/{todo_id}",
params=GetTodoParams,
response=Todo,
errors=(todo_not_found,),
)Raise the definition with request-specific safe details:
async def get_todo(params: GetTodoParams, context: AppContext) -> Todo:
todo = await context.todos.get(params.todo_id)
if todo is None:
raise AppError(todo_not_found, details={"todo_id": params.todo_id})
return todoReturn declared error headers
List any application-owned response headers on the definition, then provide their values when raising the error:
from tenchi.rate_limits import RATE_LIMITED, enforce_rate_limit
async def create_todo(request: CreateTodo, context: AppContext) -> Todo:
user = require_user(context.user)
await enforce_rate_limit(
context.rate_limits,
namespace="todos.create",
scope=user.id,
limit=10,
window_seconds=60,
)
return await context.todos.create(title=request.title)Tenchi rejects undeclared or unsafe header values, documents declared headers
in OpenAPI, and validates them in the typed client. Declare RATE_LIMITED on
the contract or a containing route group like any other application error. See
Limit application operations for the store and scoping model.
The honesty rule
An AppError maps to its declared status only when the matched contract or a
containing route group declares that definition. An undeclared application
error becomes a framework-owned 500 instead of leaking an undocumented
behavior.
The same rule applies to errors raised by authentication hooks. Every private route that can receive an authentication failure must declare it directly or through a route group.
Error envelope
Application and framework errors use one flat shape:
{
"code": "TODO_NOT_FOUND",
"message": "Todo not found",
"details": { "todo_id": "abc123" },
"request_id": "01J..."
}Every error response carries x-tenchi-error-source: app | framework.
Framework failures use stable codes for validation, media types, body limits,
timeouts, missing routes, and internal contract violations.
Client symmetry
The typed client raises declared AppError values. An undocumented status,
invalid envelope, wrong error source, mismatched media type, or invalid success
body raises UnexpectedResponseError.
Pass shared group errors to Client(errors=...) when the server declares them
at a route-group boundary. This keeps client and server error semantics
symmetric.
Treat message, details, and declared headers as API data. Do not include
exception text, SQL, tokens, or internal identifiers that callers should not
receive.