LLMOpsThe LLMOps lifecycle · MLflow 3 · FastAPI · Docker · AWS/Azure
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
41 small wins to finish your pathNext lesson →

Custom exceptions you can trace

A custom exception is the app's own error type that carries the context a failure happened in, such as the query and the index, so a caught error tells you what to fix instead of only that something broke.

Last updated: 15 Oct, 2026 · LLMOps

The Logging you can operate on lesson wrote lines an operator can read. Errors are the lines that matter most, and a bare ValueError rarely says enough. The repo gives the app its own exception/ package for this reason. This lesson builds one exception type for the shop assistant that holds the context of the failure, so the line you log and the line you catch both carry it.

An exception that holds context

AssistantError takes a message and any number of keyword fields. It keeps the fields on the error and renders them alongside the message, so printing the error shows both at once.

python
class AssistantError(Exception):
    """An error that carries the context it happened in."""

    def __init__(self, message, **context):
        super().__init__(message)
        self.context = context             # the where and what, kept on the error

    def __str__(self):
        parts = " ".join(f"{k}={v}" for k, v in self.context.items())
        return f"{self.args[0]} ({parts})" if parts else self.args[0]

Raising and catching it

A retrieval step fails and raises the error with the query, the depth and the index it was working against. The caller catches it, prints it, and reaches into the context for a single field.

ExampleThe error carries the query, depth and index that failed
from errors import AssistantError

def retrieve(query, top_k):
    # a failure deep in retrieval, raised with the inputs that caused it
    raise AssistantError("vector index is empty",
                         query=query, top_k=top_k, index="shop-faq")

try:
    retrieve("how do I return an item?", top_k=4)
except AssistantError as err:
    print("caught:", err)
    print("index was:", err.context["index"])

What the context did for the caller

  • The message named the failure and the inputs, so vector index is empty (query=... top_k=4 index=shop-faq) is enough to reproduce it without a debugger.
  • The fields stayed reachable, so the handler read err.context["index"] and could branch on it, rather than parsing a string.
  • One type covers the app's failures, so a caller catches AssistantError and knows it came from the assistant, not from a library it happened to call.
Two panels. On the left a bare exception: retrieve fails, raises a plain Exception reading error, and the caller learns only that it broke, so you guess the inputs and try to reproduce it. On the right the app's own exception: retrieve fails, raises AssistantError with the message vector index is empty plus the query, a top_k of four and the index shop-faq, and the caller reads the index straight off err.context, so you read the inputs off the error instead of guessing.

A bare exception against one that carries context

A bare exceptionA custom exception with context
Tells youThat it brokeWhat broke and on which inputs
Reproduce itGuess the inputsRead them off the error
Handle itCatch a broad typeCatch the app's own type
In a logA line of textA line plus queryable fields

Where a context-carrying error helps

  • A retrieval or model call that fails, where the query and settings are what you need to see.
  • A boundary that wraps a library, turning a third-party error into the app's own type with the call's inputs attached.
  • A log line for an operator, where the error renders its own context so the log carries the fields to filter on.
Watch out. Context is useful and also a leak risk, the same way a log field is. Attach identifiers, settings and counts, not a customer's full message or a secret, because the error text travels into logs and error trackers. Keep what you would be comfortable seeing in a dashboard, and no more.
Try it yourself
  • Add a status field to the error and read it back in the handler.
  • Wrap a call that raises a KeyError and re-raise it as an AssistantError with the key as context.
  • Log the caught error with the operational logger from the previous lesson and see its context in the line.

Little by little, you're building something great.