deepset-ai/haystack · error

`async_function` must be a coroutine function defined with `

Error message

`async_function` must be a coroutine function defined with `async def`. Got '{getattr(async_function, '__name__', repr(async_function))}'.

What it means

FunctionHook validates that `async_function` is actually a coroutine function declared with `async def`. If you pass a plain synchronous function to `async_function`, it cannot be awaited correctly during async Agent runs, so __init__ raises ValueError naming the offending callable.

Source

Thrown at haystack/hooks/from_function.py:57

        """
        Initialize the hook with a synchronous function, an async function, or both.

        :param function: The synchronous function invoked by `run`. Must be a regular function — coroutine functions
            should be passed to `async_function` instead. Either `function` or `async_function` (or both) must be set.
        :param async_function: Optional coroutine function awaited by `run_async`. When only `async_function` is set,
            `run` raises a `RuntimeError`. When only `function` is set, `run_async` calls `function`.
        :raises ValueError: If neither is set, if `function` is a coroutine function, if `async_function` is not, or
            if a provided function does not declare a `State`-typed parameter.
        """
        if function is None and async_function is None:
            raise ValueError("A FunctionHook requires at least one of `function` or `async_function` to be set.")
        if function is not None and inspect.iscoroutinefunction(function):
            raise ValueError(
                f"`function` must be a synchronous function. '{function.__name__}' is a coroutine function. "
                "Pass it as `async_function` instead."
            )
        if async_function is not None and not inspect.iscoroutinefunction(async_function):
            raise ValueError(
                f"`async_function` must be a coroutine function defined with `async def`. "
                f"Got '{getattr(async_function, '__name__', repr(async_function))}'."
            )
        for func in (function, async_function):
            if func is not None and not _takes_single_state_argument(func):
                raise ValueError(
                    f"Hook function '{func.__name__}' must take a single parameter annotated with `State` "
                    "(e.g. `def my_hook(state: State) -> None`)."
                )
        self.function = function
        self.async_function = async_function

    def run(self, state: State) -> None:
        """
        Run the synchronous function against the live `State`.

        :param state: The Agent's live `State`, mutated in place by the wrapped function.
        :raises RuntimeError: If the hook only has an `async_function`; use the Agent's async run methods instead.

View on GitHub (pinned to e318778c9b)

Solutions

  1. Declare the async hook with `async def` so it is a coroutine function
  2. If the hook is intentionally synchronous, pass it as `function` instead of `async_function`
  3. Check functools.partial/wrapped callables — pass the actual `async def` function

Example fix

// before
def my_hook(state: State) -> None: ...
hook = FunctionHook(async_function=my_hook)  # ValueError
// after
async def my_hook(state: State) -> None: ...
hook = FunctionHook(async_function=my_hook)
Defensive patterns

Strategy: validation

Validate before calling

import inspect
if async_function is not None and not inspect.iscoroutinefunction(async_function):
    raise ValueError("async_function must be declared with async def")

Type guard

def is_coroutine_function(fn) -> bool:
    return callable(fn) and inspect.iscoroutinefunction(fn)

Try / catch

try:
    hook = FunctionHook(async_function=afn, function=fn)
except ValueError as e:
    raise TypeError(f"bad hook callable: {e}") from e

Prevention

When it happens

Trigger: Calling FunctionHook(async_function=plain_sync_fn) where inspect.iscoroutinefunction(plain_sync_fn) is False; e.g. swapping parameters or passing the sync hook to the wrong keyword.

Common situations: Parameter mix-up after editing a hook declaration; forgetting `async def` when writing a new async hook; passing a bound method or partial that wraps sync code.

Related errors


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