{"record":{"id":"fd226daff87d5693","repo":"deepset-ai/haystack","slug":"async-function-must-be-a-coroutine-function-defi-fd226d","errorCode":null,"errorMessage":"`async_function` must be a coroutine function defined with `async def`. Got '{__name__ or repr}'.","messagePattern":"`async_function` must be a coroutine function defined with `async def`\\. Got '(.+?)'\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"haystack/tools/tool.py","lineNumber":126,"sourceCode":"    outputs_to_state: dict[str, dict[str, Any]] | None = None\n    async_function: Callable | None = None\n\n    def __post_init__(self) -> None:  # noqa: C901, PLR0912\n        # At least one of function / async_function must be set.\n        if self.function is None and self.async_function is None:\n            raise ValueError(f\"Tool '{self.name}' requires at least one of `function` or `async_function` to be set.\")\n\n        # `function` must be a regular (sync) function. Coroutine functions belong on `async_function`.\n        if self.function is not None and inspect.iscoroutinefunction(self.function):\n            raise ValueError(\n                f\"`function` must be a synchronous function. \"\n                f\"The function '{self.function.__name__}' is a coroutine function. \"\n                f\"Pass it as `async_function` instead.\"\n            )\n\n        # `async_function` must be a coroutine function defined with `async def`.\n        if self.async_function is not None and not inspect.iscoroutinefunction(self.async_function):\n            raise ValueError(\n                f\"`async_function` must be a coroutine function defined with `async def`. \"\n                f\"Got '{getattr(self.async_function, '__name__', repr(self.async_function))}'.\"\n            )\n\n        # Check that the parameters define a valid JSON schema\n        try:\n            Draft202012Validator.check_schema(self.parameters)\n        except SchemaError as e:\n            raise ValueError(\"The provided parameters do not define a valid JSON schema\") from e\n\n        # Validate outputs structure if provided\n        if self.outputs_to_state is not None:\n            for key, config in self.outputs_to_state.items():\n                if not isinstance(config, dict):\n                    raise TypeError(f\"outputs_to_state configuration for key '{key}' must be a dictionary\")\n                if \"source\" in config and not isinstance(config[\"source\"], str):\n                    raise ValueError(f\"outputs_to_state source for key '{key}' must be a string.\")\n                if \"handler\" in config and not callable(config[\"handler\"]):","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/deepset-ai/haystack/blob/e318778c9bf60a1963e3b5f451359655dd696c30/haystack/tools/tool.py#L108-L144","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Define the callable with `async def` (e.g. `async def my_tool(...): ...`) and pass it as async_function.","Verify the object is awaitable: `inspect.iscoroutinefunction(my_fn)` before constructing the Tool.","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)`.","If no async variant exists, pass async_function=None and use the sync function only."],"exampleFix":"// before\ndef fetch(url):\n    return requests.get(url)\nTool(name=\"fetch\", function=fetch, async_function=fetch)\n\n// after\nasync def fetch(url):\n    ...\nTool(name=\"fetch\", function=sync_fetch, async_function=fetch)","handlingStrategy":"validation","validationCode":"import inspect\nassert inspect.iscoroutinefunction(my_async_fn), \"async_function must be defined with 'async def'\"","typeGuard":"def is_coroutine_fn(fn: object) -> bool:\n    return inspect.iscoroutinefunction(fn)","tryCatchPattern":"try:\n    tool = Tool(name=\"t\", function=sync_fn, async_function=maybe_async)\nexcept ValueError as e:\n    if \"async_function\" in str(e):\n        tool = Tool(name=\"t\", function=sync_fn)\n    else:\n        raise","preventionTips":["Always declare tool callables with `async def` when using async_function","Check with inspect.iscoroutinefunction in unit tests for every tool definition","Don't pass lambdas or partials as async_function on Python < 3.11"],"tags":["python","async","tool","validation"],"backgroundTag":"not-a-coroutine-function","analyzedSha":"e318778c9bf60a1963e3b5f451359655dd696c30","analyzedAt":"2026-08-30T11:45:20.711Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}