Textualize/textual · error · RuntimeError
Can't write to the stream after it has stopped.
Error message
Can't write to the stream after it has stopped.
What it means
A RuntimeError raised by MarkdownStream.write after the stream's _run loop has finished (self._stopped). MarkdownStream is a one-shot streaming renderer: once its worker task stops (e.g. after an exception or shutdown), appending more fragments is illegal.
Source
Thrown at src/textual/widgets/_markdown.py:85
if self._task is None:
self._task = asyncio.create_task(self._run())
async def stop(self) -> None:
"""Stop the stream and await its finish."""
if self._task is not None:
self._task.cancel()
await self._task
self._task = None
self._stopped = True
async def write(self, markdown_fragment: str) -> None:
"""Append or enqueue a markdown fragment.
Args:
markdown_fragment: A string to append at the end of the document.
"""
if self._stopped:
raise RuntimeError("Can't write to the stream after it has stopped.")
if not markdown_fragment:
# Nothing to do for empty strings.
return
# Append the new fragment, and set an event to tell the _run loop to wake up
self._pending.append(markdown_fragment)
self._new_markup.set()
# Allow the task to wake up and actually display the new markdown
await asyncio.sleep(0)
async def _run(self) -> None:
"""Run a task to append markdown fragments when available."""
try:
while await self._new_markup.wait():
new_markdown = "".join(self._pending)
self._pending.clear()
self._new_markup.clear()
await asyncio.shield(self.markdown_widget.append(new_markdown))
except asyncio.CancelledError:View on GitHub (pinned to 06dbeef4bb)
Solutions
- Create a fresh MarkdownStream per response/document rather than reusing one.
- Stop/cancel producer tasks when the stream ends, and guard writes: if not stream._stopped.
- Catch RuntimeError around write in retry/cancellation paths.
- Ensure the widget's on_unmount cancels the producer feeding the stream.
Example fix
# before
stream.write(chunk) # may run after stream stopped
# after
try:
stream.write(chunk)
except RuntimeError:
stream = MarkdownStream()
await mount(stream)
stream.write(chunk) Defensive patterns
Strategy: try-catch
Validate before calling
def safe_write(stream: MarkdownStream, fragment: str) -> bool:
try:
stream.write(fragment)
return True
except RuntimeError:
return False Try / catch
try:
stream.write(chunk)
except RuntimeError:
# stream finished; create a new one or stop producing
producer.cancel() Prevention
- Create a new MarkdownStream per response/session
- Cancel producer tasks on stream completion or widget unmount
- Never reuse a stopped stream for retries
When it happens
Trigger: Writing to a MarkdownStream after its internal loop ended — e.g. after an exception in _run, after the app/widget is shutting down, or reusing a stream object for a second response.
Common situations: Streaming LLM/chat responses where a retry reuses the same MarkdownStream; writing from a background task that outlives the widget; not checking stream state on cancellation.
Related errors
- Can't animate attribute {attribute!r} on {obj!r}; attribute
- Don't know how to animate {value!r}; Can only animate <int>,
- Can't encode {datum!r}
- must be bytes
- More data expected
AI-assisted analysis of Textualize/textual@06dbeef4bb (2026-08-27).
Data as JSON: /api/errors/d1071b20b0ddd015.
Report an issue: GitHub.