alibaba/spring-ai-alibaba · error · RuntimeException

Async tool execution failed

Error message

Async tool execution failed

What it means

AsyncToolCallback.call() awaits an async tool execution and, if the underlying future/completable completed exceptionally with a cause that is neither a RuntimeException nor an Error, it wraps the cause in a RuntimeException with the message "Async tool execution failed". This keeps checked/other Throwable causes propagatable in the synchronous caller context.

Source

Thrown at spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/tool/AsyncToolCallback.java:108

	 * @param context the tool execution context
	 * @return the result string
	 * @throws RuntimeException if the async operation fails
	 * @throws ToolCancelledException if the operation was cancelled
	 */
	@Override
	default String call(String arguments, ToolContext context) {
		try {
			return callAsync(arguments, context).join();
		}
		catch (CompletionException e) {
			Throwable cause = e.getCause();
			if (cause instanceof RuntimeException re) {
				throw re;
			}
			if (cause instanceof Error err) {
				throw err;
			}
			throw new RuntimeException("Async tool execution failed", cause);
		}
		catch (CancellationException e) {
			throw new ToolCancelledException("Tool execution was cancelled", e);
		}
	}

	/**
	 * Returns whether this tool executes asynchronously.
	 * @return true (always async for this interface)
	 */
	default boolean isAsync() {
		return true;
	}

	/**
	 * Returns the timeout duration for this tool execution.
	 * @return the timeout duration, defaults to 5 minutes
	 */

View on GitHub (pinned to f82da0b50f)

Solutions

  1. Open the cause via getCause() to find the real failure — this message is only a wrapper.
  2. Throw RuntimeException subclasses from tool methods so they propagate unwrapped.
  3. Handle InterruptedException/IOException explicitly inside the tool and convert them to meaningful runtime exceptions.
  4. Catch this RuntimeException at the agent/tool-call boundary and map to a tool error result.

Example fix

// before
String result = tool.apply(input); // throws IOException
// after
String result;
try {
    result = tool.apply(input);
} catch (IOException e) {
    throw new UncheckedIOException("tool I/O failed", e);
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (cause instanceof RuntimeException || cause instanceof Error) { /* will rethrow as-is; handle earlier */ }

Type guard

if (t.getCause() instanceof RuntimeException re) { throw re; }

Try / catch

try { result = asyncTool.call(input); } catch (RuntimeException e) { Throwable cause = e.getCause(); log.error("async tool failed", cause != null ? cause : e); }

Prevention

When it happens

Trigger: A tool registered through the async callback interface throws a checked Throwable (e.g. IOException, InterruptedException) or its CompletableFuture completes exceptionally with a non-RuntimeException cause; call() unwraps the ExecutionException and re-wraps it.

Common situations: Tool methods declaring checked exceptions, I/O failures inside tools, executor/timeout failures surfaced as non-RuntimeException causes, custom tools that complete futures with Exception rather than RuntimeException.

Understand the failure class

Background: "API request failed": what wrapped HTTP errors from external APIs mean and how to find the real cause — this error's family across 29 libraries.

Related errors


AI-assisted analysis of alibaba/spring-ai-alibaba@f82da0b50f (2026-09-09). Data as JSON: /api/errors/069807052d2450ac. Report an issue: GitHub.