{"record":{"id":"d1071b20b0ddd015","repo":"Textualize/textual","slug":"can-t-write-to-the-stream-after-it-has-stopped","errorCode":null,"errorMessage":"Can't write to the stream after it has stopped.","messagePattern":"Can't write to the stream after it has stopped\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"src/textual/widgets/_markdown.py","lineNumber":85,"sourceCode":"        if self._task is None:\n            self._task = asyncio.create_task(self._run())\n\n    async def stop(self) -> None:\n        \"\"\"Stop the stream and await its finish.\"\"\"\n        if self._task is not None:\n            self._task.cancel()\n            await self._task\n            self._task = None\n            self._stopped = True\n\n    async def write(self, markdown_fragment: str) -> None:\n        \"\"\"Append or enqueue a markdown fragment.\n\n        Args:\n            markdown_fragment: A string to append at the end of the document.\n        \"\"\"\n        if self._stopped:\n            raise RuntimeError(\"Can't write to the stream after it has stopped.\")\n        if not markdown_fragment:\n            # Nothing to do for empty strings.\n            return\n        # Append the new fragment, and set an event to tell the _run loop to wake up\n        self._pending.append(markdown_fragment)\n        self._new_markup.set()\n        # Allow the task to wake up and actually display the new markdown\n        await asyncio.sleep(0)\n\n    async def _run(self) -> None:\n        \"\"\"Run a task to append markdown fragments when available.\"\"\"\n        try:\n            while await self._new_markup.wait():\n                new_markdown = \"\".join(self._pending)\n                self._pending.clear()\n                self._new_markup.clear()\n                await asyncio.shield(self.markdown_widget.append(new_markdown))\n        except asyncio.CancelledError:","sourceCodeStart":67,"sourceCodeEnd":103,"githubUrl":"https://github.com/Textualize/textual/blob/06dbeef4bb70fb718236aa418ed658ef4667a126/src/textual/widgets/_markdown.py#L67-L103","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nstream.write(chunk)  # may run after stream stopped\n\n# after\ntry:\n    stream.write(chunk)\nexcept RuntimeError:\n    stream = MarkdownStream()\n    await mount(stream)\n    stream.write(chunk)","handlingStrategy":"try-catch","validationCode":"def safe_write(stream: MarkdownStream, fragment: str) -> bool:\n    try:\n        stream.write(fragment)\n        return True\n    except RuntimeError:\n        return False","typeGuard":null,"tryCatchPattern":"try:\n    stream.write(chunk)\nexcept RuntimeError:\n    # stream finished; create a new one or stop producing\n    producer.cancel()","preventionTips":["Create a new MarkdownStream per response/session","Cancel producer tasks on stream completion or widget unmount","Never reuse a stopped stream for retries"],"tags":["textual","markdown","runtime-error","stream-closed"],"backgroundTag":"write-after-stream-closed","analyzedSha":"06dbeef4bb70fb718236aa418ed658ef4667a126","analyzedAt":"2026-08-27T02:36:57.214Z","schemaVersion":2},"datasetVersion":"2026-08-27T03:17:27.898Z"}