deepset-ai/haystack · error

`function` must be a synchronous function. '{function.__name

Error message

`function` must be a synchronous function. '{function.__name__}' is a coroutine function. Pass it as `async_function` instead.

What it means

FunctionHook's __init__ validates that the `function` argument is a plain synchronous (def) function. Passing a coroutine function (defined with `async def`) as `function` would break synchronous hook execution, so haystack raises ValueError and directs you to the `async_function` parameter instead.

Source

Thrown at haystack/hooks/from_function.py:52

    def __init__(
        self,
        function: Callable[[State], None] | None = None,
        async_function: Callable[[State], Awaitable[None]] | None = None,
    ) -> None:
        """
        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:

View on GitHub (pinned to e318778c9b)

Solutions

  1. Move the coroutine function to the `async_function` parameter: FunctionHook(function=None, async_function=my_async_fn)
  2. If the hook must stay sync-only, remove `async` from the function definition or wrap the async work with asyncio.run inside a sync function
  3. Use a factory/keyword form FunctionHook.from_function(async_function=...) if only async behavior is needed

Example fix

// before
async def my_hook(state: State) -> None: ...
hook = FunctionHook(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 function is not None and inspect.iscoroutinefunction(function):
    raise ValueError("pass an async def function as async_function instead")

Type guard

def is_sync_function(fn) -> bool:
    return callable(fn) and not inspect.iscoroutinefunction(fn) and not inspect.iscoroutine(fn)

Try / catch

try:
    hook = FunctionHook(function=fn)
except ValueError as e:
    # fall back to async_function or fix the callable
    hook = FunctionHook(async_function=fn)

Prevention

When it happens

Trigger: Calling FunctionHook(function=my_async_def_function) or FunctionHook.from_function with a coroutine function bound to the `function` parameter; inspect.iscoroutinefunction(function) is True.

Common situations: Refactoring a hook from sync to async without changing the constructor argument name; copying a hook example and adding `async` to the function; passing an async callback received from elsewhere (e.g. an async LLM handler).

Related errors


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