deepset-ai/haystack · error · ValueError

`async_function` must be a coroutine function defined with `

Error message

`async_function` must be a coroutine function defined with `async def`. Got '{__name__ or repr}'.

What it means

Tool.async_function must be a coroutine function created with `async def`. The Tool __post_init__ validator checks it with inspect.iscoroutinefunction and rejects anything else (sync functions, partials, lambdas, non-callables). This ensures Tool.invoke_async can await the function correctly.

Source

Thrown at haystack/tools/tool.py:126

    outputs_to_state: dict[str, dict[str, Any]] | None = None
    async_function: Callable | None = None

    def __post_init__(self) -> None:  # noqa: C901, PLR0912
        # At least one of function / async_function must be set.
        if self.function is None and self.async_function is None:
            raise ValueError(f"Tool '{self.name}' requires at least one of `function` or `async_function` to be set.")

        # `function` must be a regular (sync) function. Coroutine functions belong on `async_function`.
        if self.function is not None and inspect.iscoroutinefunction(self.function):
            raise ValueError(
                f"`function` must be a synchronous function. "
                f"The function '{self.function.__name__}' is a coroutine function. "
                f"Pass it as `async_function` instead."
            )

        # `async_function` must be a coroutine function defined with `async def`.
        if self.async_function is not None and not inspect.iscoroutinefunction(self.async_function):
            raise ValueError(
                f"`async_function` must be a coroutine function defined with `async def`. "
                f"Got '{getattr(self.async_function, '__name__', repr(self.async_function))}'."
            )

        # Check that the parameters define a valid JSON schema
        try:
            Draft202012Validator.check_schema(self.parameters)
        except SchemaError as e:
            raise ValueError("The provided parameters do not define a valid JSON schema") from e

        # Validate outputs structure if provided
        if self.outputs_to_state is not None:
            for key, config in self.outputs_to_state.items():
                if not isinstance(config, dict):
                    raise TypeError(f"outputs_to_state configuration for key '{key}' must be a dictionary")
                if "source" in config and not isinstance(config["source"], str):
                    raise ValueError(f"outputs_to_state source for key '{key}' must be a string.")
                if "handler" in config and not callable(config["handler"]):

View on GitHub (pinned to e318778c9b)

Solutions

  1. Define the callable with `async def` (e.g. `async def my_tool(...): ...`) and pass it as async_function.
  2. Verify the object is awaitable: `inspect.iscoroutinefunction(my_fn)` before constructing the Tool.
  3. If wrapping, use functools.partial only on Python >= 3.11 where iscoroutinefunction detects it, or wrap with `async def wrapper(*a, **kw): return await original(*a, **kw)`.
  4. If no async variant exists, pass async_function=None and use the sync function only.

Example fix

// before
def fetch(url):
    return requests.get(url)
Tool(name="fetch", function=fetch, async_function=fetch)

// after
async def fetch(url):
    ...
Tool(name="fetch", function=sync_fetch, async_function=fetch)
Defensive patterns

Strategy: validation

Validate before calling

import inspect
assert inspect.iscoroutinefunction(my_async_fn), "async_function must be defined with 'async def'"

Type guard

def is_coroutine_fn(fn: object) -> bool:
    return inspect.iscoroutinefunction(fn)

Try / catch

try:
    tool = Tool(name="t", function=sync_fn, async_function=maybe_async)
except ValueError as e:
    if "async_function" in str(e):
        tool = Tool(name="t", function=sync_fn)
    else:
        raise

Prevention

When it happens

Trigger: Constructing Tool(func=<sync>, async_function=<plain def or lambda or non-callable>) e.g. Tool(name="t", function=f, async_function=g) where g was not declared with async def; passing an async-unaware callable object implementing __call__; passing a functools.partial wrapping a coroutine function (older Python where iscoroutinefunction(partial) is False).

Common situations: Migrating a sync tool to async and reusing the same sync callable for both parameters; passing a lambda that returns a coroutine instead of being a coroutine; a typo passing func where async_function is expected; Python <3.8-style wrappers losing coroutine-ness.

Related errors


AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30). Data as JSON: /api/errors/fd226daff87d5693. Report an issue: GitHub.