Log uncaught exceptions in Python
Use sys.excepthook for top-level failures, threading.excepthook for worker threads, and explicit handling for functions and asynchronous tasks.
Install sys.excepthook to log an exception that reaches the interpreter's top level. It is a final reporting hook, not a recovery mechanism. Threads and asyncio tasks have their own exception paths, and abrupt process termination may never reach a Python hook.
Log a top-level failure
import logging
import sys
logging.basicConfig(filename="error.log", level=logging.INFO)
logger = logging.getLogger(__name__)
def handle_unhandled_exception(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
logger.critical(
"Uncaught exception",
exc_info=(exc_type, exc_value, exc_traceback),
)
sys.excepthook = handle_unhandled_exception
raise ValueError("Example unhandled failure")
Run this as a standalone script: it writes the exception and traceback to error.log, then exits unsuccessfully. SystemExit does not use this hook. An interactive shell, notebook, or application server may manage exception reporting itself, so test the actual execution environment.
Handle worker-thread exceptions
Python 3.8+ provides threading.excepthook for exceptions escaping Thread.run():
import threading
def handle_thread_exception(args):
if args.exc_type is SystemExit:
return
thread_name = args.thread.name if args.thread is not None else "unknown"
logger.error(
"Uncaught exception in thread %s",
thread_name,
exc_info=(args.exc_type, args.exc_value, args.exc_traceback),
)
threading.excepthook = handle_thread_exception
A failed worker thread does not necessarily terminate the entire process. Do not retain the exception or thread object indefinitely in the hook; doing so can retain object graphs. Executor-based workers may store failures in a Future instead, which you should inspect or await.
Log and re-raise at a function boundary
import functools
def log_exceptions(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception:
logger.exception("%s failed", func.__name__)
raise
return wrapper
@log_exceptions
def process_data(value):
return int(value)
This decorator catches ordinary exceptions from a synchronous function and preserves the failure for its caller. It does not catch errors that occur later when a returned generator is consumed or a returned coroutine is awaited. An async decorator needs an async def wrapper that awaits the function inside its try block.
Choose a logging boundary deliberately: the same exception may otherwise be recorded by this wrapper and again by a top-level hook.
FAQ
Can the hook keep the program running? Normal top-level exception reporting occurs after stack unwinding. Returning from the hook does not resume the failed operation. Catch a recoverable error around that operation instead.
How does logger.exception differ from logger.error? logger.exception() logs at ERROR and includes the current exception information. Use it while handling an exception. The hook already receives an exception tuple, so it passes that tuple explicitly.
What about asyncio? Await tasks and handle failures where ownership is clear. An event loop's set_exception_handler() handles certain loop-reported failures, including unobserved task failures when they are reported. Reporting is not guaranteed to wait until loop shutdown; it can occur when an unobserved future or task is finalized. It is not a substitute for retrieving task results.
References
Contact us
Join the community
to join the community
Try Guance
Start online and pay only for what you use.
Get startedChoose a Guance plan