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
- Move the coroutine function to the `async_function` parameter: FunctionHook(function=None, async_function=my_async_fn)
- 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
- 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
- Check inspect.iscoroutinefunction before wiring hooks
- Keep sync and async hook functions in clearly named pairs (e.g. _sync/_async)
- Add a unit test constructing every hook you register
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
- `async_function` must be a coroutine function defined with `
- Hook function '{func.__name__}' must take a single parameter
- This FunctionHook only has an `async_function` and cannot ru
- Expected one ToolExecutionDecision for each tool call, but r
- No unused ToolExecutionDecision matches tool call {tc.tool_n
AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30).
Data as JSON: /api/errors/9a447f166f973bc7.
Report an issue: GitHub.