diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 60f278c4efe..dcf39485531 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -5,6 +5,7 @@ import asyncio import dataclasses import logging import threading +import traceback from typing import Any from homeassistant import bootstrap @@ -86,9 +87,15 @@ def _async_loop_exception_handler(_: Any, context: dict[str, Any]) -> None: if exception := context.get("exception"): kwargs["exc_info"] = (type(exception), exception, exception.__traceback__) - logging.getLogger(__package__).error( - "Error doing job: %s", context["message"], **kwargs # type: ignore - ) + logger = logging.getLogger(__package__) + if source_traceback := context.get("source_traceback"): + stack_summary = "".join(traceback.format_list(source_traceback)) + logger.error( + "Error doing job: %s: %s", context["message"], stack_summary, **kwargs # type: ignore + ) + return + + logger.error("Error doing job: %s", context["message"], **kwargs) # type: ignore async def setup_and_run_hass(runtime_config: RuntimeConfig) -> int: diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 5f991d15bbf..e9327c0255a 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -1,5 +1,11 @@ """List of tests that have uncaught exceptions today. Will be shrunk over time.""" IGNORE_UNCAUGHT_EXCEPTIONS = [ + ( + # This test explicitly throws an uncaught exception + # and should not be removed. + "tests.test_runner", + "test_unhandled_exception_traceback", + ), ( "test_homeassistant_bridge", "test_homeassistant_bridge_fan_setup", diff --git a/tests/test_runner.py b/tests/test_runner.py index 0e38cef0fff..20136275b74 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -117,3 +117,23 @@ def test_run_does_not_block_forever_with_shielded_task(hass, tmpdir, caplog): assert ( "Task could not be canceled and was still running after shutdown" in caplog.text ) + + +async def test_unhandled_exception_traceback(hass, caplog): + """Test an unhandled exception gets a traceback in debug mode.""" + + async def _unhandled_exception(): + raise Exception("This is unhandled") + + try: + hass.loop.set_debug(True) + asyncio.create_task(_unhandled_exception()) + finally: + hass.loop.set_debug(False) + + await asyncio.sleep(0) + await asyncio.sleep(0) + + assert "Task exception was never retrieved" in caplog.text + assert "This is unhandled" in caplog.text + assert "_unhandled_exception" in caplog.text