Skip to content

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 todo

Return 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.

Details are public

Treat message, details, and declared headers as API data. Do not include exception text, SQL, tokens, or internal identifiers that callers should not receive.