{"record":{"id":"2f79b41e5766a343","repo":"Textualize/textual","slug":"can-not-create-a-worker-from-a-non-async-function","errorCode":null,"errorMessage":"Can not create a worker from a non-async function unless `thread=True` is set on the work decorator.","messagePattern":"Can not create a worker from a non-async function unless `thread=True` is set on the work decorator\\.","errorType":"exception","errorClass":"WorkerDeclarationError","httpStatus":null,"severity":"error","filePath":"src/textual/_work_decorator.py","lineNumber":113,"sourceCode":"        exclusive: Cancel all workers in the same group.\n        description: Readable description of the worker for debugging purposes.\n            By default, it uses a string representation of the decorated method\n            and its arguments.\n        thread: Mark the method as a thread worker.\n    \"\"\"\n\n    def decorator(\n        method: (\n            Callable[DecoratorParamSpec, ReturnType]\n            | Callable[DecoratorParamSpec, Coroutine[None, None, ReturnType]]\n        ),\n    ) -> Callable[DecoratorParamSpec, Worker[ReturnType]]:\n        \"\"\"The decorator.\"\"\"\n\n        # Methods that aren't async *must* be marked as being a thread\n        # worker.\n        if not iscoroutinefunction(method) and not thread:\n            raise WorkerDeclarationError(\n                \"Can not create a worker from a non-async function unless `thread=True` is set on the work decorator.\"\n            )\n\n        @wraps(method)\n        def decorated(\n            *args: DecoratorParamSpec.args, **kwargs: DecoratorParamSpec.kwargs\n        ) -> Worker[ReturnType]:\n            \"\"\"The replaced callable.\"\"\"\n            from textual.dom import DOMNode\n\n            self = args[0]\n            assert isinstance(self, DOMNode)\n\n            if description is not None:\n                debug_description = description\n            else:\n                try:\n                    positional_arguments = \", \".join(repr(arg) for arg in args[1:])","sourceCodeStart":95,"sourceCodeEnd":131,"githubUrl":"https://github.com/Textualize/textual/blob/06dbeef4bb70fb718236aa418ed658ef4667a126/src/textual/_work_decorator.py#L95-L131","documentation":"WorkerDeclarationError raised by the @work decorator when the decorated method is a plain (non-async) function and thread=True was not passed. Textual workers must either be coroutines or explicitly declared as thread workers.","triggerScenarios":"Applying @work to a regular def method without arguments: @work def load(self): ...; the decorator body checks iscoroutinefunction(method) and thread.","commonSituations":"Wrapping blocking/synchronous I/O (requests, file reads) with @work and forgetting the thread flag; converting an async method to sync during refactoring without updating the decorator.","solutions":["Add thread=True if the function is intentionally blocking: @work(thread=True)","Make the method async if the work can be awaited (async def with await inside)","For exclusive/group behavior, combine with thread=True: @work(thread=True, exclusive=True)"],"exampleFix":"# before\n@work\ndef load_data(self):\n    return requests.get(url).json()\n\n# after\n@work(thread=True)\ndef load_data(self):\n    return requests.get(url).json()","handlingStrategy":"validation","validationCode":"import inspect\nassert inspect.iscoroutinefunction(method) or THREAD_FLAG, 'non-async @work requires thread=True'","typeGuard":"def is_thread_worker_safe(fn, thread: bool) -> bool:\n    import inspect\n    return inspect.iscoroutinefunction(fn) or thread","tryCatchPattern":null,"preventionTips":["Pair every blocking def with @work(thread=True)","Add a lint check: flag def methods decorated with bare @work"],"tags":["worker","async","decorator","textual"],"backgroundTag":"async-decorator-misuse","analyzedSha":"06dbeef4bb70fb718236aa418ed658ef4667a126","analyzedAt":"2026-08-27T02:36:57.214Z","schemaVersion":2},"datasetVersion":"2026-08-27T03:17:27.898Z"}