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
- Declare the async hook with `async def` so it is a coroutine function
- If the hook is intentionally synchronous, pass it as `function` instead of `async_function`
- 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
- Always declare async hooks with `async def`
- Double-check keyword argument names when constructing FunctionHook
- Unwrap partials/wrappers so the underlying async def is passed
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
- `function` must be a synchronous function. '{function.__name
- 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/2acd25f8311c1a90.
Report an issue: GitHub.