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.errors import AppError, ErrorDef
write_temporarily_unavailable = ErrorDef(
code="WRITE_TEMPORARILY_UNAVAILABLE",
status=503,
message="Todo writes are temporarily unavailable",
headers=("Retry-After",),
)
create_todo_contract = contract(
method="POST",
path="/todos",
request=CreateTodo,
response=Todo,
errors=(write_temporarily_unavailable,),
)
async def create_todo(request: CreateTodo, context: AppContext) -> Todo:
if not await context.todos.accepting_writes():
raise AppError(
write_temporarily_unavailable,
headers={"Retry-After": "30"},
)
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. The built-in RATE_LIMITED
error uses the same mechanism for its Retry-After value. 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.