Textual crashes on application error
When my custom application code raises an exception in on_mount, Textual clears the screen (which hides the original error) and then crashes itself.
When debugging #247 I found that the way Textual crashes is rather unsatisfying way upon exit (Screenshot 1). Scrolling up reveals parts of the beautifully rendered error message, which unfortunately is cut off (Screenshot 2).
| Screenshot 1 | Screenshot 2 |
|---|---|
![]() |
![]() |
Based on my limited testing this affects both Windows and Linux, both on main and the css branch[^1].
[^1]: The simple example on the css branch only renders a black screen at the moment, but the crash behavior is the same.
Steps to reproduce
Repro: Insert raise RuntimeError in the get_markdown callback in examples/simple.py:
https://github.com/Textualize/textual/blob/cfefb36ee4dcd4ac3b5c5bde5c040ebeb55f11e6/examples/simple.py#L30-L35
Additional Information
The amount of info that is cut off depends on the terminal size, presumably because textual clears the current screen upon exit and everything above the fold stays.
terminal width 120x40
PS C:\Users\user\git\textual\examples> python .\simple.py ╭───────────────────────────────────────── Traceback (most recent call last) ──────────────────────────────────────────╮ │ │ │ C:\Python310\lib\site-packages\textual\message_pump.py:306 in on_callback │ │ │ │ 303 │ │ return await self.post_message(message) │ │ 304 │ │ │ 305 │ async def on_callback(self, event: events.Callback) -> None: │ │ ❱ 306 │ │ await event.callback() │ │ 307 │ │ │ 308 │ def emit_no_wait(self, message: Message) -> bool: │ │ 309 │ │ if self._parent: │ │ │ │ ╭───────────────────────────────────────────────────── locals ─────────────────────────────────────────────────────╮ │Traceback (most recent call last): File "C:\Python310\lib\site-packages\textual_timer.py", line 89, in _run async def _run(self) -> None: asyncio.exceptions.CancelledError
During handling of the above exception, another exception occurred:
Traceback (most recent call last): File "C:\Python310\lib\site-packages\textual\app.py", line 204, in run_app await app.process_messages() File "C:\Python310\lib\site-packages\textual\app.py", line 311, in process_messages await self.animator.stop() File "C:\Python310\lib\site-packages\textual_animator.py", line 119, in stop await self._timer.stop() File "C:\Python310\lib\site-packages\textual_timer.py", line 79, in stop await self._task asyncio.exceptions.CancelledError
During handling of the above exception, another exception occurred:
Traceback (most recent call last): File "C:\Users\user\git\textual\examples\simple.py", line 38, in
MyApp.run(title="Simple App", log="textual.log") File "C:\Python310\lib\site-packages\textual\app.py", line 206, in run asyncio.run(run_app()) File "C:\Python310\lib\asyncio\runners.py", line 44, in run return loop.run_until_complete(main) File "C:\Python310\lib\asyncio\base_events.py", line 641, in run_until_complete return future.result() asyncio.exceptions.CancelledError
terminal width 80x40
PS C:\Users\user\git\textual\examples> python .\simple.py ╭───────────────────── Traceback (most recent call last) ──────────────────────╮ │ │ │ C:\Python310\lib\site-packages\textual\message_pump.py:306 in on_callback │ │ │ │ 303 │ │ return await self.post_message(message) │ │ 304 │ │ │ 305 │ async def on_callback(self, event: events.Callback) -> None: │ │ ❱ 306 │ │ await event.callback() │ │ 307 │ │ │ 308 │ def emit_no_wait(self, message: Message) -> bool: │ │ 309 │ │ if self._parent: │ │ │ │ ╭───────────────────────────────── locals ─────────────────────────────────╮ │ │ │ event = Callback( │ │ │ │ │ callback=functools.partial(.get_markdown at 0x000001D291BED630>, │ │ │ │ 'richreadme.md') │ │ Traceback (most recent call last): File "C:\Python310\lib\site-packages\textual_timer.py", line 89, in _run async def _run(self) -> None: asyncio.exceptions.CancelledError
During handling of the above exception, another exception occurred:
Traceback (most recent call last): File "C:\Python310\lib\site-packages\textual\app.py", line 204, in run_app await app.process_messages() File "C:\Python310\lib\site-packages\textual\app.py", line 311, in process_messages await self.animator.stop() File "C:\Python310\lib\site-packages\textual_animator.py", line 119, in stop await self._timer.stop() File "C:\Python310\lib\site-packages\textual_timer.py", line 79, in stop await self._task asyncio.exceptions.CancelledError
During handling of the above exception, another exception occurred:
Traceback (most recent call last): File "C:\Users\user\git\textual\examples\simple.py", line 38, in
MyApp.run(title="Simple App", log="textual.log") File "C:\Python310\lib\site-packages\textual\app.py", line 206, in run asyncio.run(run_app()) File "C:\Python310\lib\asyncio\runners.py", line 44, in run return loop.run_until_complete(main) File "C:\Python310\lib\asyncio\base_events.py", line 641, in run_until_complete return future.result() asyncio.exceptions.CancelledError
terminal width 80x20
PS C:\Users\user\git\textual\examples> python .\simple.py ╭───────────────────── Traceback (most recent call last) ──────────────────────╮ │ │ │ C:\Python310\lib\site-packages\textual\message_pump.py:306 in on_callback │ │ │ │ 303 │ │ return await self.post_message(message) │ │ 304 │ │ │ 305 │ async def on_callback(self, event: events.Callback) -> None: │ │ ❱ 306 │ │ await event.callback() │ │ 307 │ │ │ 308 │ def emit_no_wait(self, message: Message) -> bool: │ │ 309 │ │ if self._parent: │ │ │ │ ╭───────────────────────────────── locals ─────────────────────────────────╮ │ │ │ event = Callback( │ │ │ │ │ callback=functools.partial(.get_markdown at 0x000001CA9AA1D630>, │ │ │ │ 'richreadme.md') │ │ │ │ ) │ │ │ │ self = MyApp(title='Simple App') │ │ │ ╰──────────────────────────────────────────────────────────────────────────╯ │ │ C:\Users\user\git\textual\examples\simple.py:32 in get_markdown │ │ │ │ 29 │ │ │ │ 30 │ │ async def get_markdown(filename: str) -> None: │ │ 31 │ │ │ with open(filename, "r") as fh: │ │ ❱ 32 │ │ │ │ readme = Markdown(fh.read(), hyperlinks=True) │ │ 33 │ │ │ await body.update(readme) │ │ 34 │ │ │ │ 35 │ │ await self.call_later(get_markdown, "richreadme.md") │ │ │ │ ╭───────────────────────────────── locals ─────────────────────────────────╮ │ │ │ body = ScrollView(name='ScrollView#1') │ │ │ │ fh = <_io.textiowrapper name="richreadme.md" mode="r" encoding="cp1252"> │ │ │ │ filename = 'richreadme.md' │ │ │ ╰──────────────────────────────────────────────────────────────────────────╯ │ │ │ Traceback (most recent call last): File "C:\Python310\lib\site-packages\textual_timer.py", line 89, in _run async def _run(self) -> None: asyncio.exceptions.CancelledError
During handling of the above exception, another exception occurred:
Traceback (most recent call last): File "C:\Python310\lib\site-packages\textual\app.py", line 204, in run_app await app.process_messages() File "C:\Python310\lib\site-packages\textual\app.py", line 311, in process_messages await self.animator.stop() File "C:\Python310\lib\site-packages\textual_animator.py", line 119, in stop await self._timer.stop() File "C:\Python310\lib\site-packages\textual_timer.py", line 79, in stop await self._task asyncio.exceptions.CancelledError
During handling of the above exception, another exception occurred:
Traceback (most recent call last): File "C:\Users\user\git\textual\examples\simple.py", line 38, in
MyApp.run(title="Simple App", log="textual.log") File "C:\Python310\lib\site-packages\textual\app.py", line 206, in run asyncio.run(run_app()) File "C:\Python310\lib\asyncio\runners.py", line 44, in run return loop.run_until_complete(main) File "C:\Python310\lib\asyncio\base_events.py", line 641, in run_until_complete return future.result() asyncio.exceptions.CancelledError
I've noticed the same thing. It's not clear to me if debugging in Textual is so difficult because of Textual itself, or because of the whole asyncio thing. But it makes for slow going!
Seeing the same thing on my end @mhils. Haven't yet gotten a chance to dig into the root cause behind this - but guessing it's some premature runloop termination that's also short circuiting the basic drawing layer setup. A temporary workaround is to allow the graphics code to paint before our mounting code interrupts it:
from asyncio import sleep
async def on_mount(self, event: events.Mount) -> None:
"""Create and dock the widgets."""
await sleep(0.001)
# A scrollview to contain the markdown file
body = ScrollView(gutter=1)
...
https://github.com/Textualize/textual/wiki/Sorry-we-closed-your-issue
Did we solve your problem?
Glad we could help!

