How to attach a request ID to exception messages in Python
This code shows how to enrich exception messages with contextual request IDs using context variables, making error logs more traceable across concurrent requests.
Python code
22 linesimport logging
from contextvars import ContextVar
request_id_var = ContextVar("request_id", default="unknown")
def add_request_id(exc: Exception) -> Exception:
exc.args = (f"request_id={request_id_var.get()} | {exc.args[0]}" if exc.args else f"request_id={request_id_var.get()}",) + exc.args[1:]
return exc
def handle_request(request_id: str) -> None:
request_id_var.set(request_id)
try:
raise ValueError("Database connection failed")
except ValueError as e:
raise add_request_id(e)
if __name__ == "__main__":
request_id_var.set("req-12345")
try:
handle_request("req-12345")
except ValueError as e:
logging.error("Error caught: %s", e)
Output
ERROR:root:Error caught: request_id=req-12345 | Database connection failed
How it works
The ContextVar provides a thread-safe way to store per-request data (like request IDs) that persists across async boundaries. When an exception is raised, the add_request_id function mutates exc.args to prepend the request ID to the message while preserving any remaining arguments. This approach is lightweight and doesn't require global state, making it ideal for concurrent web server contexts. Since the same request_id_var is reused, every exception raised within the same request context automatically gets tagged with the correct ID.
Common mistakes
- Forgetting to reset the ContextVar after a request completes, causing stale IDs to leak into unrelated operations
- Assuming exc.args always has a string as its first element — need to handle empty args case
- Not using ContextVar.copy_context() in async frameworks, leading to incorrect ID propagation
- Modifying exception args instead of using exception groups or custom exception classes for complex cases
Variations
- Use `functools.wraps` with a decorator pattern to automatically wrap exceptions from any function
- Create a custom exception subclass (e.g., `RequestScopedError`) that carries the request ID as a separate attribute
Real-world use cases
- In a Flask or FastAPI middleware, attaching request IDs to all errors before logging them for structured log aggregation
- In background job workers (e.g., Celery), tagging each task's exceptions with a correlation ID for distributed tracing
- In an async web server handling multiple concurrent requests, ensuring each error log shows which request it belongs to
Sponsored
More from Errors & debugging
- Catch RecursionError and Fail Gracefully in Python easy
- Catch ValueError and print friendly message in Python easy
- Collect Multiple Validation Errors in Python Before Raising medium
- Handle ValueError and ZeroDivisionError in Python with try except easy
- How to Add a Correlation ID to Logging Records in Python medium
- How to Assert Preconditions with Descriptive Messages in Python easy
Keep learning
Related tutorials and quizzes for this topic.