Textualize/textual · error · WorkerDeclarationError
Can not create a worker from a non-async function unless `th
Error message
Can not create a worker from a non-async function unless `thread=True` is set on the work decorator.
What it means
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.
Source
Thrown at src/textual/_work_decorator.py:113
exclusive: Cancel all workers in the same group.
description: Readable description of the worker for debugging purposes.
By default, it uses a string representation of the decorated method
and its arguments.
thread: Mark the method as a thread worker.
"""
def decorator(
method: (
Callable[DecoratorParamSpec, ReturnType]
| Callable[DecoratorParamSpec, Coroutine[None, None, ReturnType]]
),
) -> Callable[DecoratorParamSpec, Worker[ReturnType]]:
"""The decorator."""
# Methods that aren't async *must* be marked as being a thread
# worker.
if not iscoroutinefunction(method) and not thread:
raise WorkerDeclarationError(
"Can not create a worker from a non-async function unless `thread=True` is set on the work decorator."
)
@wraps(method)
def decorated(
*args: DecoratorParamSpec.args, **kwargs: DecoratorParamSpec.kwargs
) -> Worker[ReturnType]:
"""The replaced callable."""
from textual.dom import DOMNode
self = args[0]
assert isinstance(self, DOMNode)
if description is not None:
debug_description = description
else:
try:
positional_arguments = ", ".join(repr(arg) for arg in args[1:])View on GitHub (pinned to 06dbeef4bb)
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)
Example fix
# before
@work
def load_data(self):
return requests.get(url).json()
# after
@work(thread=True)
def load_data(self):
return requests.get(url).json() Defensive patterns
Strategy: validation
Validate before calling
import inspect assert inspect.iscoroutinefunction(method) or THREAD_FLAG, 'non-async @work requires thread=True'
Type guard
def is_thread_worker_safe(fn, thread: bool) -> bool:
import inspect
return inspect.iscoroutinefunction(fn) or thread Prevention
- Pair every blocking def with @work(thread=True)
- Add a lint check: flag def methods decorated with bare @work
When it happens
Trigger: Applying @work to a regular def method without arguments: @work def load(self): ...; the decorator body checks iscoroutinefunction(method) and thread.
Common situations: 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.
Related errors
- Request to run a non-async function as an async worker
- Unsupported attempt to run an async worker
- Can't call worker.wait from within the worker function!
- Worker must be started before calling this method.
- Worker was cancelled, and did not complete.
AI-assisted analysis of Textualize/textual@06dbeef4bb (2026-08-27).
Data as JSON: /api/errors/2f79b41e5766a343.
Report an issue: GitHub.