{"record":{"id":"08a1302033853358","repo":"Textualize/textual","slug":"the-call-from-thread-method-must-run-in-a-differ","errorCode":null,"errorMessage":"The `call_from_thread` method must run in a different thread from the app","messagePattern":"The `call_from_thread` method must run in a different thread from the app","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"src/textual/app.py","lineNumber":1822,"sourceCode":"\n        Args:\n            callback: A callable to run.\n            *args: Arguments to the callback.\n            **kwargs: Keyword arguments for the callback.\n\n        Raises:\n            RuntimeError: If the app isn't running or if this method is called from the same\n                thread where the app is running.\n\n        Returns:\n            The result of the callback.\n        \"\"\"\n\n        if self._loop is None:\n            raise RuntimeError(\"App is not running\")\n\n        if self._thread_id == threading.get_ident():\n            raise RuntimeError(\n                \"The `call_from_thread` method must run in a different thread from the app\"\n            )\n\n        callback_with_args = partial(callback, *args, **kwargs)\n\n        async def run_callback() -> CallThreadReturnType:\n            \"\"\"Run the callback, set the result or error on the future.\"\"\"\n            with self._context():\n                return await invoke(callback_with_args)\n\n        # Post the message to the main loop\n        future: Future[CallThreadReturnType] = asyncio.run_coroutine_threadsafe(\n            run_callback(), loop=self._loop\n        )\n        result = future.result()\n        return result\n\n    def action_change_theme(self) -> None:","sourceCodeStart":1804,"sourceCodeEnd":1840,"githubUrl":"https://github.com/Textualize/textual/blob/06dbeef4bb70fb718236aa418ed658ef4667a126/src/textual/app.py#L1804-L1840","documentation":"RuntimeError raised by App.call_from_thread when executed on the app's own thread (thread ids match). The API exists specifically to hop from a worker thread onto the event-loop thread; calling it from the loop thread is a misuse.","triggerScenarios":"Invoking app.call_from_thread(...) inside an async method, on_mount, or any code already running on the app thread.","commonSituations":"Shared helper functions used both from async handlers and thread workers; copy-pasted thread-updating code into normal event handlers.","solutions":["From async context just call the function directly, or await an asyncio helper","Restrict call_from_thread to @work(thread=True) worker bodies","In shared helpers, branch on app._thread_id == threading.get_ident()"],"exampleFix":"# before\nasync def on_click(self):\n    self.app.call_from_thread(self.update_ui)  # same thread → error\n\n# after\nasync def on_click(self):\n    self.update_ui()","handlingStrategy":"validation","validationCode":"import threading\nif threading.get_ident() != app._thread_id:\n    app.call_from_thread(cb)\nelse:\n    cb()  # already on the loop thread","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Call call_from_thread only inside @work(thread=True) bodies","From async code, invoke UI updates directly"],"tags":["threading","asyncio","textual"],"backgroundTag":"wrong-thread-call","analyzedSha":"06dbeef4bb70fb718236aa418ed658ef4667a126","analyzedAt":"2026-08-27T02:36:57.214Z","schemaVersion":2},"datasetVersion":"2026-08-27T03:17:27.898Z"}