Lesson 14 — handle errors and logging¶
What you will build¶
Three failures, produced on purpose, and the habit of reading them: every error Xyberos raises derives
from XyberosError, and each one names the thing you have to fix rather than the place that noticed
it.
What you need¶
The setup from lesson 1. This lesson uses nothing beyond the core package.
The code¶
"""Lesson 14 — the errors you will meet, and how to read them.
Every error Xyberos raises derives from ``XyberosError``, and each one names the thing you have to
fix rather than the place it noticed. This module produces three of them on purpose and prints the
name and the message, plus the correlation id that ties a log line to the request or task it belongs
to.
"""
from __future__ import annotations
import asyncio
from typing import Annotated
from xyberos import App, ConfigurationError, Inject, LifecycleStateError, Settings
from xyberos.errors import DependencyCycleError
from xyberos.runtime.context import bind_correlation_id, get_correlation_id
class First:
"""Depends on ``Second``, which depends on ``First``: a cycle, not a missing provider."""
def __init__(self, other: Annotated[Second, Inject()]) -> None:
self.other = other
class Second:
"""The other half of the cycle."""
def __init__(self, other: Annotated[First, Inject()]) -> None:
self.other = other
def report(label: str, error: BaseException) -> None:
"""Print an error the way a reader should learn to read one."""
print(f"{label}: {type(error).__name__}: {error}")
async def main() -> None:
"""Trigger three failures and show the message each one gives."""
try:
await App().resolve(First)
except LifecycleStateError as error:
report("resolving before startup", error)
try:
Settings.load(environ={"XYBEROS_APP__UNKNOWN": "1"})
except ConfigurationError as error:
report("a misspelled setting", error)
cycled = App()
cycled.provide(First)
cycled.provide(Second)
try:
cycled.validate()
except DependencyCycleError as error:
report("two services that need each other", error)
with bind_correlation_id("lesson-14"):
print("correlation id in this block:", get_correlation_id())
if __name__ == "__main__":
asyncio.run(main())
How to run it¶
What it prints¶
resolving before startup: LifecycleStateError: cannot resolve dependencies while application state is 'new'
a misspelled setting: ConfigurationError: unknown setting(s) at app: unknown
two services that need each other: DependencyCycleError: dependency cycle detected: First -> Second -> First
correlation id in this block: lesson-14
The taxonomy, and when you meet each one¶
| Error | Raised when | What to do |
|---|---|---|
ConfigurationError |
a setting is unknown or cannot be parsed | fix the file or the name; the message names the field and never the value |
RegistrationError |
app.provide was called with something contradictory |
fix the registration, before anything runs |
MissingProviderError |
a declared dependency has no provider | register it, or stop declaring it |
DependencyCycleError |
two providers need each other | break the cycle with a third contract, or pass one in |
ScopeViolationError |
a longer-lived provider needs a shorter-lived one | change one of the scopes (lesson 9) |
DependencyResolutionError |
construction failed | read error.__cause__; the framework error is the wrapper |
LifecycleStateError |
an operation was attempted in the wrong state | start the application, or stop asking it to work while new |
HttpError |
a handler wants a status code and a safe message | raise it; the adapter maps it, and never leaks internals |
Two properties are worth more than the table. The message names the subject: dependency cycle
detected: First -> Second -> First is a description of the fix. And the runtime refuses early:
a missing provider is a startup failure, not a first-request failure.
Logging¶
The runtime ships helpers rather than a logging configuration, because where logs go is the application's decision:
| Helper | What it does |
|---|---|
bind_correlation_id("...") |
binds an id for the duration of a block, so every log line in it is traceable |
get_correlation_id() |
reads it back — lesson-14 above, from the last line of output |
bind_context(**values) |
adds diagnostic values, returned by get_context() |
ContextLogFilter |
attaches the current context to every LogRecord as xyberos_context, so a formatter can print it |
create_task_in_context(...) |
creates a task with the caller's context, including on Python 3.10 |
Attach the filter to your handler and the correlation id appears on every record without being passed
around: handler.addFilter(ContextLogFilter()), with a formatter of
"%(levelname)s %(xyberos_context)s %(message)s".
Context is diagnostic only. It is not identity and not authorization: a tenant in a context variable is ambient authority, which is exactly the failure mode ADR 0014 refuses. Authorization travels as an argument to a service, never through a context variable.
Common mistakes¶
- Catching
Exceptionand logging the message. Catch the framework error you expect, and let the rest propagate — a wiring failure is a startup failure, and swallowing it turns a clear message into a confusing one far from its cause. - Logging a secret. Configuration errors deliberately print the field rather than the value; your
own logging should follow that rule, which is what
LoggingPolicy.metadata_only()inxyberos.aienforces for prompt and completion content. - Using context for authorization. See above: it is a diagnostic channel, and treating it as authority is how one request reads another tenant's rows.
Exercise¶
Add a fourth failure to the module: a service whose factory returns the wrong type.
Show the answer
Registering `app.provide(Contract, factory=lambda: object())` fails at *registration* with `RegistrationError`, naming both what the factory returns and the contract it was registered against — before anything runs. That is the pattern to learn from this lesson: the earlier a mistake can be caught, the more useful the message. Registration catches the shape, `validate()` catches the graph, startup catches construction, and a request catches only what is left.Next¶
That is the Fundamentals path. The roadmap shows the lessons still in progress and what comes after them — the traditional path, which builds a database-backed application from this same starting point.