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

  1. Add thread=True if the function is intentionally blocking: @work(thread=True)
  2. Make the method async if the work can be awaited (async def with await inside)
  3. 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

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


AI-assisted analysis of Textualize/textual@06dbeef4bb (2026-08-27). Data as JSON: /api/errors/2f79b41e5766a343. Report an issue: GitHub.